summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorMike Lewis <mlewis@gitlab.com>2019-06-07 20:13:17 +0000
committerMike Lewis <mlewis@gitlab.com>2019-06-07 20:13:17 +0000
commit99df0218f82b851b017bd0eea1b8351dc89df6ed (patch)
treeb01f884fbd1418dd5465fc1741f1620061ae8c5c /spec
parent3eea6906747d10bea501426febaf15d2c209e06a (diff)
parente07b2b277f79bc25cdce22ca2defba1ba80791aa (diff)
downloadgitlab-ce-99df0218f82b851b017bd0eea1b8351dc89df6ed.tar.gz
Merge branch 'master' into 'docs/fix-example-dot-net'
# Conflicts: # doc/user/project/clusters/serverless/index.md
Diffstat (limited to 'spec')
-rw-r--r--spec/bin/changelog_spec.rb2
-rw-r--r--spec/config/mail_room_spec.rb2
-rw-r--r--spec/config/object_store_settings_spec.rb4
-rw-r--r--spec/config/settings_spec.rb2
-rw-r--r--spec/controllers/abuse_reports_controller_spec.rb2
-rw-r--r--spec/controllers/acme_challenges_controller_spec.rb44
-rw-r--r--spec/controllers/admin/appearances_controller_spec.rb48
-rw-r--r--spec/controllers/admin/application_settings_controller_spec.rb31
-rw-r--r--spec/controllers/admin/applications_controller_spec.rb2
-rw-r--r--spec/controllers/admin/clusters/applications_controller_spec.rb149
-rw-r--r--spec/controllers/admin/clusters_controller_spec.rb540
-rw-r--r--spec/controllers/admin/dashboard_controller_spec.rb2
-rw-r--r--spec/controllers/admin/gitaly_servers_controller_spec.rb2
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb8
-rw-r--r--spec/controllers/admin/health_check_controller_spec.rb2
-rw-r--r--spec/controllers/admin/hooks_controller_spec.rb2
-rw-r--r--spec/controllers/admin/identities_controller_spec.rb2
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb2
-rw-r--r--spec/controllers/admin/projects_controller_spec.rb12
-rw-r--r--spec/controllers/admin/runners_controller_spec.rb21
-rw-r--r--spec/controllers/admin/services_controller_spec.rb2
-rw-r--r--spec/controllers/admin/spam_logs_controller_spec.rb2
-rw-r--r--spec/controllers/admin/users_controller_spec.rb2
-rw-r--r--spec/controllers/application_controller_spec.rb66
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb33
-rw-r--r--spec/controllers/boards/issues_controller_spec.rb28
-rw-r--r--spec/controllers/boards/lists_controller_spec.rb2
-rw-r--r--spec/controllers/concerns/checks_collaboration_spec.rb2
-rw-r--r--spec/controllers/concerns/continue_params_spec.rb2
-rw-r--r--spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb2
-rw-r--r--spec/controllers/concerns/enforces_admin_authentication_spec.rb40
-rw-r--r--spec/controllers/concerns/group_tree_spec.rb2
-rw-r--r--spec/controllers/concerns/import_url_params_spec.rb56
-rw-r--r--spec/controllers/concerns/internal_redirect_spec.rb2
-rw-r--r--spec/controllers/concerns/issuable_collections_spec.rb112
-rw-r--r--spec/controllers/concerns/lfs_request_spec.rb2
-rw-r--r--spec/controllers/concerns/project_unauthorized_spec.rb53
-rw-r--r--spec/controllers/concerns/routable_actions_spec.rb156
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb7
-rw-r--r--spec/controllers/dashboard/groups_controller_spec.rb52
-rw-r--r--spec/controllers/dashboard/labels_controller_spec.rb8
-rw-r--r--spec/controllers/dashboard/milestones_controller_spec.rb12
-rw-r--r--spec/controllers/dashboard/projects_controller_spec.rb52
-rw-r--r--spec/controllers/dashboard/todos_controller_spec.rb8
-rw-r--r--spec/controllers/dashboard_controller_spec.rb35
-rw-r--r--spec/controllers/explore/groups_controller_spec.rb2
-rw-r--r--spec/controllers/explore/projects_controller_spec.rb32
-rw-r--r--spec/controllers/google_api/authorizations_controller_spec.rb62
-rw-r--r--spec/controllers/graphql_controller_spec.rb113
-rw-r--r--spec/controllers/groups/avatars_controller_spec.rb12
-rw-r--r--spec/controllers/groups/boards_controller_spec.rb32
-rw-r--r--spec/controllers/groups/children_controller_spec.rb14
-rw-r--r--spec/controllers/groups/clusters/applications_controller_spec.rb100
-rw-r--r--spec/controllers/groups/clusters_controller_spec.rb29
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb88
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb8
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb8
-rw-r--r--spec/controllers/groups/runners_controller_spec.rb2
-rw-r--r--spec/controllers/groups/settings/ci_cd_controller_spec.rb90
-rw-r--r--spec/controllers/groups/shared_projects_controller_spec.rb4
-rw-r--r--spec/controllers/groups/uploads_controller_spec.rb2
-rw-r--r--spec/controllers/groups/variables_controller_spec.rb36
-rw-r--r--spec/controllers/groups_controller_spec.rb184
-rw-r--r--spec/controllers/health_check_controller_spec.rb2
-rw-r--r--spec/controllers/health_controller_spec.rb2
-rw-r--r--spec/controllers/help_controller_spec.rb2
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb2
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb2
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb2
-rw-r--r--spec/controllers/import/gitea_controller_spec.rb2
-rw-r--r--spec/controllers/import/github_controller_spec.rb2
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb2
-rw-r--r--spec/controllers/import/gitlab_projects_controller_spec.rb2
-rw-r--r--spec/controllers/import/google_code_controller_spec.rb2
-rw-r--r--spec/controllers/import/phabricator_controller_spec.rb92
-rw-r--r--spec/controllers/invites_controller_spec.rb2
-rw-r--r--spec/controllers/ldap/omniauth_callbacks_controller_spec.rb4
-rw-r--r--spec/controllers/metrics_controller_spec.rb2
-rw-r--r--spec/controllers/notification_settings_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb2
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb41
-rw-r--r--spec/controllers/passwords_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/accounts_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/avatars_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/emails_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/keys_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/notifications_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/personal_access_tokens_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb2
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb2
-rw-r--r--spec/controllers/profiles_controller_spec.rb2
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/autocomplete_sources_controller_spec.rb31
-rw-r--r--spec/controllers/projects/avatars_controller_spec.rb4
-rw-r--r--spec/controllers/projects/badges_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blame_controller_spec.rb2
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb64
-rw-r--r--spec/controllers/projects/boards_controller_spec.rb28
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb2
-rw-r--r--spec/controllers/projects/ci/lints_controller_spec.rb22
-rw-r--r--spec/controllers/projects/clusters/applications_controller_spec.rb159
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb35
-rw-r--r--spec/controllers/projects/commit_controller_spec.rb2
-rw-r--r--spec/controllers/projects/commits_controller_spec.rb6
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb4
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb2
-rw-r--r--spec/controllers/projects/deploy_keys_controller_spec.rb2
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb2
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb2
-rw-r--r--spec/controllers/projects/environments/prometheus_api_controller_spec.rb185
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb165
-rw-r--r--spec/controllers/projects/find_file_controller_spec.rb2
-rw-r--r--spec/controllers/projects/forks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/git_http_controller_spec.rb15
-rw-r--r--spec/controllers/projects/graphs_controller_spec.rb18
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb39
-rw-r--r--spec/controllers/projects/hooks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/imports_controller_spec.rb17
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb44
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb96
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb2
-rw-r--r--spec/controllers/projects/mattermosts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/conflicts_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/creations_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests/diffs_controller_spec.rb2
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb32
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb36
-rw-r--r--spec/controllers/projects/mirrors_controller_spec.rb8
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb33
-rw-r--r--spec/controllers/projects/pages_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipeline_schedules_controller_spec.rb5
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb6
-rw-r--r--spec/controllers/projects/pipelines_settings_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb4
-rw-r--r--spec/controllers/projects/prometheus/metrics_controller_spec.rb2
-rw-r--r--spec/controllers/projects/protected_branches_controller_spec.rb2
-rw-r--r--spec/controllers/projects/protected_tags_controller_spec.rb2
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb4
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb6
-rw-r--r--spec/controllers/projects/registry/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/registry/tags_controller_spec.rb2
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/runners_controller_spec.rb2
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb101
-rw-r--r--spec/controllers/projects/services_controller_spec.rb8
-rw-r--r--spec/controllers/projects/settings/ci_cd_controller_spec.rb28
-rw-r--r--spec/controllers/projects/settings/integrations_controller_spec.rb2
-rw-r--r--spec/controllers/projects/settings/operations_controller_spec.rb215
-rw-r--r--spec/controllers/projects/settings/repository_controller_spec.rb2
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb2
-rw-r--r--spec/controllers/projects/stages_controller_spec.rb72
-rw-r--r--spec/controllers/projects/tags/releases_controller_spec.rb61
-rw-r--r--spec/controllers/projects/tags_controller_spec.rb2
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb2
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb2
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb24
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb2
-rw-r--r--spec/controllers/projects/variables_controller_spec.rb2
-rw-r--r--spec/controllers/projects/wikis_controller_spec.rb24
-rw-r--r--spec/controllers/projects_controller_spec.rb77
-rw-r--r--spec/controllers/registrations_controller_spec.rb27
-rw-r--r--spec/controllers/root_controller_spec.rb2
-rw-r--r--spec/controllers/search_controller_spec.rb65
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb111
-rw-r--r--spec/controllers/sessions_controller_spec.rb36
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb2
-rw-r--r--spec/controllers/snippets_controller_spec.rb6
-rw-r--r--spec/controllers/uploads_controller_spec.rb2
-rw-r--r--spec/controllers/user_callouts_controller_spec.rb10
-rw-r--r--spec/controllers/users/terms_controller_spec.rb2
-rw-r--r--spec/controllers/users_controller_spec.rb48
-rw-r--r--spec/db/importers/common_metrics_importer_spec.rb44
-rw-r--r--spec/db/schema_spec.rb4
-rw-r--r--spec/factories/ci/builds.rb20
-rw-r--r--spec/factories/ci/group_variables.rb1
-rw-r--r--spec/factories/ci/job_artifacts.rb18
-rw-r--r--spec/factories/ci/pipeline_schedule.rb10
-rw-r--r--spec/factories/ci/pipeline_schedule_variables.rb1
-rw-r--r--spec/factories/ci/pipelines.rb14
-rw-r--r--spec/factories/ci/variables.rb1
-rw-r--r--spec/factories/clusters/applications/helm.rb19
-rw-r--r--spec/factories/clusters/clusters.rb9
-rw-r--r--spec/factories/clusters/providers/gcp.rb4
-rw-r--r--spec/factories/commit_statuses.rb4
-rw-r--r--spec/factories/deployments.rb2
-rw-r--r--spec/factories/groups.rb9
-rw-r--r--spec/factories/merge_requests.rb60
-rw-r--r--spec/factories/pages_domain_acme_orders.rb17
-rw-r--r--spec/factories/pages_domains.rb8
-rw-r--r--spec/factories/pool_repositories.rb1
-rw-r--r--spec/factories/project_auto_devops.rb1
-rw-r--r--spec/factories/project_daily_statistics.rb8
-rw-r--r--spec/factories/project_metrics_settings.rb8
-rw-r--r--spec/factories/projects.rb19
-rw-r--r--spec/factories/services.rb6
-rw-r--r--spec/factories/suggestions.rb6
-rw-r--r--spec/factories/uploads.rb7
-rw-r--r--spec/features/admin/admin_appearance_spec.rb2
-rw-r--r--spec/features/admin/admin_browses_logs_spec.rb1
-rw-r--r--spec/features/admin/admin_hooks_spec.rb10
-rw-r--r--spec/features/admin/admin_runners_spec.rb50
-rw-r--r--spec/features/admin/admin_sees_project_statistics_spec.rb29
-rw-r--r--spec/features/admin/admin_sees_projects_statistics_spec.rb20
-rw-r--r--spec/features/admin/admin_settings_spec.rb51
-rw-r--r--spec/features/admin/admin_users_spec.rb7
-rw-r--r--spec/features/atom/dashboard_issues_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb2
-rw-r--r--spec/features/boards/sidebar_spec.rb43
-rw-r--r--spec/features/clusters/cluster_detail_page_spec.rb89
-rw-r--r--spec/features/commits/user_uses_quick_actions_spec.rb23
-rw-r--r--spec/features/commits_spec.rb6
-rw-r--r--spec/features/cycle_analytics_spec.rb19
-rw-r--r--spec/features/dashboard/activity_spec.rb17
-rw-r--r--spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb38
-rw-r--r--spec/features/dashboard/issuables_counter_spec.rb4
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb29
-rw-r--r--spec/features/dashboard/projects_spec.rb14
-rw-r--r--spec/features/dashboard/todos/todos_spec.rb20
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb189
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb4
-rw-r--r--spec/features/explore/groups_list_spec.rb6
-rw-r--r--spec/features/global_search_spec.rb14
-rw-r--r--spec/features/group_variables_spec.rb2
-rw-r--r--spec/features/groups/clusters/user_spec.rb3
-rw-r--r--spec/features/groups/group_page_with_external_authorization_service_spec.rb58
-rw-r--r--spec/features/groups/group_settings_spec.rb8
-rw-r--r--spec/features/groups/members/leave_group_spec.rb26
-rw-r--r--spec/features/groups/merge_requests_spec.rb4
-rw-r--r--spec/features/groups/settings/ci_cd_spec.rb45
-rw-r--r--spec/features/groups_spec.rb2
-rw-r--r--spec/features/help_pages_spec.rb6
-rw-r--r--spec/features/ide/user_opens_merge_request_spec.rb21
-rw-r--r--spec/features/instance_statistics/conversational_development_index_spec.rb2
-rw-r--r--spec/features/issuables/issuable_list_spec.rb2
-rw-r--r--spec/features/issuables/markdown_references/internal_references_spec.rb2
-rw-r--r--spec/features/issuables/shortcuts_issuable_spec.rb2
-rw-r--r--spec/features/issuables/sorting_list_spec.rb28
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_emoji_spec.rb6
-rw-r--r--spec/features/issues/filtered_search/dropdown_hint_spec.rb34
-rw-r--r--spec/features/issues/filtered_search/dropdown_milestone_spec.rb8
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb21
-rw-r--r--spec/features/issues/form_spec.rb8
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb84
-rw-r--r--spec/features/issues/issue_detail_spec.rb2
-rw-r--r--spec/features/issues/issue_sidebar_spec.rb2
-rw-r--r--spec/features/issues/user_creates_branch_and_merge_request_spec.rb40
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb18
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb4
-rw-r--r--spec/features/issues/user_uses_quick_actions_spec.rb387
-rw-r--r--spec/features/issues_spec.rb29
-rw-r--r--spec/features/labels_hierarchy_spec.rb4
-rw-r--r--spec/features/markdown/copy_as_gfm_spec.rb11
-rw-r--r--spec/features/markdown/gitlab_flavored_markdown_spec.rb3
-rw-r--r--spec/features/merge_request/maintainer_edits_fork_spec.rb4
-rw-r--r--spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb2
-rw-r--r--spec/features/merge_request/user_creates_image_diff_notes_spec.rb116
-rw-r--r--spec/features/merge_request/user_creates_merge_request_spec.rb6
-rw-r--r--spec/features/merge_request/user_creates_mr_spec.rb15
-rw-r--r--spec/features/merge_request/user_edits_mr_spec.rb18
-rw-r--r--spec/features/merge_request/user_merges_merge_request_spec.rb2
-rw-r--r--spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb25
-rw-r--r--spec/features/merge_request/user_posts_diff_notes_spec.rb2
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb14
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb15
-rw-r--r--spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb38
-rw-r--r--spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb86
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb148
-rw-r--r--spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb6
-rw-r--r--spec/features/merge_request/user_sees_versions_spec.rb8
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb115
-rw-r--r--spec/features/merge_request/user_uses_quick_actions_spec.rb236
-rw-r--r--spec/features/merge_request/user_views_diffs_spec.rb10
-rw-r--r--spec/features/merge_request/user_views_open_merge_request_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_assignees_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb2
-rw-r--r--spec/features/merge_requests/user_filters_by_target_branch_spec.rb45
-rw-r--r--spec/features/merge_requests/user_lists_merge_requests_spec.rb8
-rw-r--r--spec/features/merge_requests/user_mass_updates_spec.rb3
-rw-r--r--spec/features/milestone_spec.rb28
-rw-r--r--spec/features/oauth_login_spec.rb2
-rw-r--r--spec/features/profiles/active_sessions_spec.rb48
-rw-r--r--spec/features/profiles/user_edit_preferences_spec.rb32
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb32
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb2
-rw-r--r--spec/features/project_variables_spec.rb4
-rw-r--r--spec/features/projects/artifacts/user_browses_artifacts_spec.rb6
-rw-r--r--spec/features/projects/badges/pipeline_badge_spec.rb19
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb7
-rw-r--r--spec/features/projects/blobs/edit_spec.rb13
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/classification_label_on_project_pages_spec.rb22
-rw-r--r--spec/features/projects/clusters/applications_spec.rb96
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb2
-rw-r--r--spec/features/projects/clusters/user_spec.rb3
-rw-r--r--spec/features/projects/clusters_spec.rb2
-rw-r--r--spec/features/projects/commit/comments/user_adds_comment_spec.rb4
-rw-r--r--spec/features/projects/commit/mini_pipeline_graph_spec.rb2
-rw-r--r--spec/features/projects/commits/user_browses_commits_spec.rb2
-rw-r--r--spec/features/projects/environments/environment_spec.rb10
-rw-r--r--spec/features/projects/environments/environments_spec.rb35
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/files/files_sort_submodules_with_folders_spec.rb2
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb1
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb1
-rw-r--r--spec/features/projects/files/user_browses_lfs_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_directory_spec.rb2
-rw-r--r--spec/features/projects/files/user_creates_files_spec.rb3
-rw-r--r--spec/features/projects/files/user_deletes_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_edits_files_spec.rb5
-rw-r--r--spec/features/projects/files/user_replaces_files_spec.rb2
-rw-r--r--spec/features/projects/files/user_uploads_files_spec.rb2
-rw-r--r--spec/features/projects/forks/fork_list_spec.rb35
-rw-r--r--spec/features/projects/graph_spec.rb2
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb1
-rw-r--r--spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb128
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb2
-rw-r--r--spec/features/projects/jobs/user_browses_job_spec.rb4
-rw-r--r--spec/features/projects/jobs/user_browses_jobs_spec.rb6
-rw-r--r--spec/features/projects/jobs_spec.rb119
-rw-r--r--spec/features/projects/labels/update_prioritization_spec.rb20
-rw-r--r--spec/features/projects/labels/user_promotes_label_spec.rb34
-rw-r--r--spec/features/projects/labels/user_removes_labels_spec.rb5
-rw-r--r--spec/features/projects/labels/user_views_labels_spec.rb5
-rw-r--r--spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb9
-rw-r--r--spec/features/projects/members/invite_group_spec.rb4
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb13
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb3
-rw-r--r--spec/features/projects/new_project_spec.rb49
-rw-r--r--spec/features/projects/pages_lets_encrypt_spec.rb131
-rw-r--r--spec/features/projects/pages_spec.rb40
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb8
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb246
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb147
-rw-r--r--spec/features/projects/serverless/functions_spec.rb59
-rw-r--r--spec/features/projects/services/disable_triggers_spec.rb5
-rw-r--r--spec/features/projects/services/user_activates_hipchat_spec.rb40
-rw-r--r--spec/features/projects/services/user_activates_issue_tracker_spec.rb35
-rw-r--r--spec/features/projects/services/user_activates_youtrack_spec.rb89
-rw-r--r--spec/features/projects/services/user_views_services_spec.rb3
-rw-r--r--spec/features/projects/settings/external_authorization_service_settings_spec.rb21
-rw-r--r--spec/features/projects/settings/forked_project_settings_spec.rb1
-rw-r--r--spec/features/projects/settings/operations_settings_spec.rb83
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb31
-rw-r--r--spec/features/projects/settings/project_settings_spec.rb43
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb31
-rw-r--r--spec/features/projects/settings/user_manages_group_links_spec.rb1
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb29
-rw-r--r--spec/features/projects/settings/user_renames_a_project_spec.rb37
-rw-r--r--spec/features/projects/show/download_buttons_spec.rb3
-rw-r--r--spec/features/projects/show/user_manages_notifications_spec.rb10
-rw-r--r--spec/features/projects/show/user_sees_collaboration_links_spec.rb1
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb58
-rw-r--r--spec/features/projects/snippets/user_comments_on_snippet_spec.rb4
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/tree/tree_show_spec.rb1
-rw-r--r--spec/features/projects/user_creates_project_spec.rb27
-rw-r--r--spec/features/projects/user_sees_sidebar_spec.rb102
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb8
-rw-r--r--spec/features/projects/wiki/user_views_wiki_pages_spec.rb89
-rw-r--r--spec/features/projects_spec.rb19
-rw-r--r--spec/features/protected_branches_spec.rb26
-rw-r--r--spec/features/protected_tags_spec.rb13
-rw-r--r--spec/features/raven_js_spec.rb4
-rw-r--r--spec/features/search/user_searches_for_users_spec.rb83
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb36
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb4
-rw-r--r--spec/features/security/group/private_access_spec.rb21
-rw-r--r--spec/features/security/profile_access_spec.rb2
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb4
-rw-r--r--spec/features/snippets/user_creates_snippet_spec.rb2
-rw-r--r--spec/features/tags/master_deletes_tag_spec.rb2
-rw-r--r--spec/features/task_lists_spec.rb8
-rw-r--r--spec/features/u2f_spec.rb20
-rw-r--r--spec/features/user_opens_link_to_comment.rb33
-rw-r--r--spec/features/user_sees_revert_modal_spec.rb6
-rw-r--r--spec/features/users/login_spec.rb45
-rw-r--r--spec/features/users/overview_spec.rb6
-rw-r--r--spec/features/users/show_spec.rb22
-rw-r--r--spec/features/users/signup_spec.rb44
-rw-r--r--spec/finders/admin/runners_finder_spec.rb8
-rw-r--r--spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb66
-rw-r--r--spec/finders/autocomplete/users_finder_spec.rb16
-rw-r--r--spec/finders/cluster_ancestors_finder_spec.rb29
-rw-r--r--spec/finders/clusters/knative_services_finder_spec.rb105
-rw-r--r--spec/finders/group_projects_finder_spec.rb39
-rw-r--r--spec/finders/issues_finder_spec.rb210
-rw-r--r--spec/finders/labels_finder_spec.rb13
-rw-r--r--spec/finders/members_finder_spec.rb44
-rw-r--r--spec/finders/merge_requests_finder_spec.rb533
-rw-r--r--spec/finders/milestones_finder_spec.rb2
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb80
-rw-r--r--spec/finders/snippets_finder_spec.rb253
-rw-r--r--spec/finders/todos_finder_spec.rb7
-rw-r--r--spec/finders/users_finder_spec.rb31
-rw-r--r--spec/fixtures/api/graphql/introspection.graphql92
-rw-r--r--spec/fixtures/api/schemas/board.json3
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json4
-rw-r--r--spec/fixtures/api/schemas/entities/issue.json5
-rw-r--r--spec/fixtures/api/schemas/entities/issue_board.json3
-rw-r--r--spec/fixtures/api/schemas/entities/issue_boards.json3
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_basic.json14
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_sidebar.json3
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json7
-rw-r--r--spec/fixtures/api/schemas/entities/test_case.json1
-rw-r--r--spec/fixtures/api/schemas/environment.json7
-rw-r--r--spec/fixtures/api/schemas/issue.json5
-rw-r--r--spec/fixtures/api/schemas/issues.json3
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule.json2
-rw-r--r--spec/fixtures/api/schemas/pipeline_schedule_variable.json10
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/artifact.json16
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/artifact_file.json12
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/deployment.json32
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/environment.json23
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/job.json64
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/label_basic.json24
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_request.json135
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/merge_requests.json122
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json3
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json32
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release.json35
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json22
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/release/tag_release.json12
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/releases.json4
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/runner.json24
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/tag.json2
-rw-r--r--spec/fixtures/api/schemas/variable.json2
-rw-r--r--spec/fixtures/blockquote_fence_after.md16
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml36
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json13
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json20
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json17
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json20
-rw-r--r--spec/fixtures/passphrase_x509_certificate.crt27
-rw-r--r--spec/fixtures/passphrase_x509_certificate_pk.key54
-rw-r--r--spec/fixtures/phabricator_responses/auth_failed.json1
-rw-r--r--spec/fixtures/phabricator_responses/maniphest.search.json98
-rw-r--r--spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json422
-rw-r--r--spec/fixtures/security-reports/master/gl-dast-report.json74
-rw-r--r--spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json104
-rw-r--r--spec/fixtures/security-reports/remediations/remediation.patch180
-rw-r--r--spec/fixtures/security-reports/remediations/yarn.lock104
-rw-r--r--spec/fixtures/trace/sample_trace18
-rw-r--r--spec/fixtures/valid.po3
-rw-r--r--spec/fixtures/x509_certificate.crt27
-rw-r--r--spec/fixtures/x509_certificate_pk.key51
-rw-r--r--spec/frontend/.eslintrc.yml11
-rw-r--r--spec/frontend/__mocks__/file_mock.js1
-rw-r--r--spec/frontend/activities_spec.js (renamed from spec/javascripts/activities_spec.js)5
-rw-r--r--spec/frontend/api_spec.js (renamed from spec/javascripts/api_spec.js)54
-rw-r--r--spec/frontend/autosave_spec.js (renamed from spec/javascripts/autosave_spec.js)19
-rw-r--r--spec/frontend/behaviors/secret_values_spec.js (renamed from spec/javascripts/behaviors/secret_values_spec.js)0
-rw-r--r--spec/frontend/blob/blob_fork_suggestion_spec.js (renamed from spec/javascripts/blob/blob_fork_suggestion_spec.js)0
-rw-r--r--spec/frontend/boards/modal_store_spec.js (renamed from spec/javascripts/boards/modal_store_spec.js)4
-rw-r--r--spec/frontend/boards/stores/actions_spec.js67
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js91
-rw-r--r--spec/frontend/boards/stores/state_spec.js11
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js387
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js (renamed from spec/javascripts/clusters/components/application_row_spec.js)337
-rw-r--r--spec/frontend/clusters/components/applications_spec.js (renamed from spec/javascripts/clusters/components/applications_spec.js)176
-rw-r--r--spec/frontend/clusters/components/knative_domain_editor_spec.js141
-rw-r--r--spec/frontend/clusters/components/uninstall_application_button_spec.js32
-rw-r--r--spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js56
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js162
-rw-r--r--spec/frontend/clusters/services/mock_data.js (renamed from spec/javascripts/clusters/services/mock_data.js)22
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js (renamed from spec/javascripts/clusters/stores/clusters_store_spec.js)82
-rw-r--r--spec/frontend/cycle_analytics/limit_warning_component_spec.js (renamed from spec/javascripts/cycle_analytics/limit_warning_component_spec.js)0
-rw-r--r--spec/frontend/diffs/components/diff_stats_spec.js (renamed from spec/javascripts/diffs/components/diff_stats_spec.js)0
-rw-r--r--spec/frontend/diffs/components/edit_button_spec.js61
-rw-r--r--spec/frontend/diffs/components/hidden_files_warning_spec.js48
-rw-r--r--spec/frontend/diffs/components/no_changes_spec.js (renamed from spec/javascripts/diffs/components/no_changes_spec.js)0
-rw-r--r--spec/frontend/environment.js67
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js (renamed from spec/javascripts/error_tracking/components/error_tracking_list_spec.js)22
-rw-r--r--spec/frontend/error_tracking/store/mutation_spec.js (renamed from spec/javascripts/error_tracking/store/mutation_spec.js)0
-rw-r--r--spec/frontend/filtered_search/filtered_search_token_keys_spec.js (renamed from spec/javascripts/filtered_search/filtered_search_token_keys_spec.js)0
-rw-r--r--spec/frontend/filtered_search/services/recent_searches_service_error_spec.js (renamed from spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js)2
-rw-r--r--spec/frontend/filtered_search/stores/recent_searches_store_spec.js (renamed from spec/javascripts/filtered_search/stores/recent_searches_store_spec.js)0
-rw-r--r--spec/frontend/frequent_items/store/getters_spec.js (renamed from spec/javascripts/frequent_items/store/getters_spec.js)0
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js155
-rw-r--r--spec/frontend/helpers/class_spec_helper.js9
-rw-r--r--spec/frontend/helpers/fixtures.js34
-rw-r--r--spec/frontend/helpers/jest_helpers.js24
-rw-r--r--spec/frontend/helpers/jquery.js6
-rw-r--r--spec/frontend/helpers/local_storage_helper.js41
-rw-r--r--spec/frontend/helpers/locale_helper.js11
-rw-r--r--spec/frontend/helpers/monitor_helper_spec.js45
-rw-r--r--spec/frontend/helpers/scroll_into_view_promise.js28
-rw-r--r--spec/frontend/helpers/set_timeout_promise_helper.js4
-rw-r--r--spec/frontend/helpers/text_helper.js (renamed from spec/javascripts/helpers/vue_component_helper.js)0
-rw-r--r--spec/frontend/helpers/timeout.js59
-rw-r--r--spec/frontend/helpers/user_mock_data_helper.js14
-rw-r--r--spec/frontend/helpers/vue_mount_component_helper.js38
-rw-r--r--spec/frontend/helpers/vue_resource_helper.js11
-rw-r--r--spec/frontend/helpers/vue_test_utils_helper.js21
-rw-r--r--spec/frontend/helpers/vuex_action_helper.js104
-rw-r--r--spec/frontend/helpers/wait_for_attribute_change.js16
-rw-r--r--spec/frontend/helpers/wait_for_promises.js1
-rw-r--r--spec/frontend/ide/lib/common/disposable_spec.js (renamed from spec/javascripts/ide/lib/common/disposable_spec.js)2
-rw-r--r--spec/frontend/ide/lib/diff/diff_spec.js77
-rw-r--r--spec/frontend/ide/lib/editor_options_spec.js (renamed from spec/javascripts/ide/lib/editor_options_spec.js)2
-rw-r--r--spec/frontend/ide/lib/files_spec.js78
-rw-r--r--spec/frontend/ide/stores/modules/commit/mutations_spec.js (renamed from spec/javascripts/ide/stores/modules/commit/mutations_spec.js)33
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/getters_spec.js (renamed from spec/javascripts/ide/stores/modules/file_templates/getters_spec.js)0
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/mutations_spec.js88
-rw-r--r--spec/frontend/ide/stores/modules/pane/getters_spec.js (renamed from spec/javascripts/ide/stores/modules/pane/getters_spec.js)0
-rw-r--r--spec/frontend/ide/stores/modules/pane/mutations_spec.js (renamed from spec/javascripts/ide/stores/modules/pane/mutations_spec.js)0
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/getters_spec.js (renamed from spec/javascripts/ide/stores/modules/pipelines/getters_spec.js)0
-rw-r--r--spec/frontend/ide/stores/mutations/branch_spec.js75
-rw-r--r--spec/frontend/ide/stores/mutations/merge_request_spec.js (renamed from spec/javascripts/ide/stores/mutations/merge_request_spec.js)18
-rw-r--r--spec/frontend/ide/stores/mutations/project_spec.js23
-rw-r--r--spec/frontend/image_diff/view_types_spec.js (renamed from spec/javascripts/image_diff/view_types_spec.js)0
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js185
-rw-r--r--spec/frontend/import_projects/components/imported_project_table_row_spec.js (renamed from spec/javascripts/import_projects/components/imported_project_table_row_spec.js)26
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js (renamed from spec/javascripts/import_projects/components/provider_repo_table_row_spec.js)91
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js (renamed from spec/javascripts/import_projects/store/actions_spec.js)4
-rw-r--r--spec/frontend/import_projects/store/getters_spec.js (renamed from spec/javascripts/import_projects/store/getters_spec.js)0
-rw-r--r--spec/frontend/import_projects/store/mutations_spec.js (renamed from spec/javascripts/import_projects/store/mutations_spec.js)4
-rw-r--r--spec/frontend/issuable_suggestions/components/app_spec.js (renamed from spec/javascripts/issuable_suggestions/components/app_spec.js)0
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js (renamed from spec/javascripts/issuable_suggestions/components/item_spec.js)0
-rw-r--r--spec/frontend/issuable_suggestions/mock_data.js (renamed from spec/javascripts/issuable_suggestions/mock_data.js)0
-rw-r--r--spec/frontend/jobs/components/empty_state_spec.js (renamed from spec/javascripts/jobs/components/empty_state_spec.js)0
-rw-r--r--spec/frontend/jobs/components/erased_block_spec.js (renamed from spec/javascripts/jobs/components/erased_block_spec.js)0
-rw-r--r--spec/frontend/jobs/components/sidebar_detail_row_spec.js (renamed from spec/javascripts/jobs/components/sidebar_detail_row_spec.js)0
-rw-r--r--spec/frontend/jobs/components/stuck_block_spec.js (renamed from spec/javascripts/jobs/components/stuck_block_spec.js)0
-rw-r--r--spec/frontend/jobs/store/getters_spec.js (renamed from spec/javascripts/jobs/store/getters_spec.js)55
-rw-r--r--spec/frontend/jobs/store/mutations_spec.js (renamed from spec/javascripts/jobs/store/mutations_spec.js)38
-rw-r--r--spec/frontend/labels_select_spec.js116
-rw-r--r--spec/frontend/lib/utils/autosave_spec.js64
-rw-r--r--spec/frontend/lib/utils/cache_spec.js (renamed from spec/javascripts/lib/utils/cache_spec.js)0
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js (renamed from spec/javascripts/lib/utils/datetime_utility_spec.js)22
-rw-r--r--spec/frontend/lib/utils/grammar_spec.js (renamed from spec/javascripts/lib/utils/grammar_spec.js)0
-rw-r--r--spec/frontend/lib/utils/image_utility_spec.js (renamed from spec/javascripts/lib/utils/image_utility_spec.js)0
-rw-r--r--spec/frontend/lib/utils/number_utility_spec.js (renamed from spec/javascripts/lib/utils/number_utility_spec.js)22
-rw-r--r--spec/frontend/lib/utils/text_utility_spec.js (renamed from spec/javascripts/lib/utils/text_utility_spec.js)47
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js (renamed from spec/javascripts/lib/utils/url_utility_spec.js)84
-rw-r--r--spec/frontend/locale/ensure_single_line_spec.js (renamed from spec/javascripts/locale/ensure_single_line_spec.js)0
-rw-r--r--spec/frontend/locale/sprintf_spec.js (renamed from spec/javascripts/locale/sprintf_spec.js)0
-rw-r--r--spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap95
-rw-r--r--spec/frontend/mr_popover/index_spec.js46
-rw-r--r--spec/frontend/mr_popover/mr_popover_spec.js61
-rw-r--r--spec/frontend/notebook/lib/highlight_spec.js (renamed from spec/javascripts/notebook/lib/highlight_spec.js)0
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js104
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js139
-rw-r--r--spec/frontend/notes/components/discussion_reply_placeholder_spec.js (renamed from spec/javascripts/notes/components/discussion_reply_placeholder_spec.js)0
-rw-r--r--spec/frontend/notes/components/discussion_resolve_button_spec.js (renamed from spec/javascripts/notes/components/discussion_resolve_button_spec.js)0
-rw-r--r--spec/frontend/notes/components/note_app_spec.js (renamed from spec/javascripts/notes/components/note_app_spec.js)204
-rw-r--r--spec/frontend/notes/components/note_attachment_spec.js (renamed from spec/javascripts/notes/components/note_attachment_spec.js)0
-rw-r--r--spec/frontend/notes/components/note_edited_text_spec.js (renamed from spec/javascripts/notes/components/note_edited_text_spec.js)0
-rw-r--r--spec/frontend/notes/old_notes_spec.js (renamed from spec/javascripts/notes_spec.js)493
-rw-r--r--spec/frontend/notes/stores/utils_spec.js17
-rw-r--r--spec/frontend/operation_settings/components/external_dashboard_spec.js166
-rw-r--r--spec/frontend/operation_settings/store/mutations_spec.js19
-rw-r--r--spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js (renamed from spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js)10
-rw-r--r--spec/frontend/pages/profiles/show/emoji_menu_spec.js14
-rw-r--r--spec/frontend/performance_bar/services/performance_bar_service_spec.js (renamed from spec/javascripts/performance_bar/services/performance_bar_service_spec.js)0
-rw-r--r--spec/frontend/pipelines/blank_state_spec.js (renamed from spec/javascripts/pipelines/blank_state_spec.js)0
-rw-r--r--spec/frontend/pipelines/empty_state_spec.js (renamed from spec/javascripts/pipelines/empty_state_spec.js)0
-rw-r--r--spec/frontend/pipelines/pipeline_store_spec.js (renamed from spec/javascripts/pipelines/pipeline_store_spec.js)0
-rw-r--r--spec/frontend/pipelines/pipelines_store_spec.js (renamed from spec/javascripts/pipelines/pipelines_store_spec.js)0
-rw-r--r--spec/frontend/registry/getters_spec.js (renamed from spec/javascripts/registry/getters_spec.js)0
-rw-r--r--spec/frontend/reports/components/report_item_spec.js33
-rw-r--r--spec/frontend/reports/components/report_link_spec.js (renamed from spec/javascripts/reports/components/report_link_spec.js)0
-rw-r--r--spec/frontend/reports/components/report_section_spec.js (renamed from spec/javascripts/reports/components/report_section_spec.js)42
-rw-r--r--spec/frontend/reports/store/utils_spec.js (renamed from spec/javascripts/reports/store/utils_spec.js)0
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js44
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap35
-rw-r--r--spec/frontend/repository/components/table/index_spec.js80
-rw-r--r--spec/frontend/repository/components/table/parent_row_spec.js64
-rw-r--r--spec/frontend/repository/components/table/row_spec.js101
-rw-r--r--spec/frontend/repository/router_spec.js23
-rw-r--r--spec/frontend/repository/utils/icon_spec.js23
-rw-r--r--spec/frontend/repository/utils/title_spec.js15
-rw-r--r--spec/frontend/serverless/components/area_spec.js122
-rw-r--r--spec/frontend/serverless/components/environment_row_spec.js (renamed from spec/javascripts/serverless/components/environment_row_spec.js)45
-rw-r--r--spec/frontend/serverless/components/function_details_spec.js117
-rw-r--r--spec/frontend/serverless/components/function_row_spec.js32
-rw-r--r--spec/frontend/serverless/components/functions_spec.js130
-rw-r--r--spec/frontend/serverless/components/missing_prometheus_spec.js41
-rw-r--r--spec/frontend/serverless/components/pod_box_spec.js23
-rw-r--r--spec/frontend/serverless/components/url_spec.js (renamed from spec/javascripts/serverless/components/url_spec.js)23
-rw-r--r--spec/frontend/serverless/mock_data.js142
-rw-r--r--spec/frontend/serverless/store/actions_spec.js90
-rw-r--r--spec/frontend/serverless/store/getters_spec.js43
-rw-r--r--spec/frontend/serverless/store/mutations_spec.js86
-rw-r--r--spec/frontend/serverless/utils.js20
-rw-r--r--spec/frontend/sidebar/confidential_edit_buttons_spec.js (renamed from spec/javascripts/sidebar/confidential_edit_buttons_spec.js)0
-rw-r--r--spec/frontend/sidebar/confidential_edit_form_buttons_spec.js (renamed from spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js)0
-rw-r--r--spec/frontend/sidebar/lock/edit_form_spec.js (renamed from spec/javascripts/sidebar/lock/edit_form_spec.js)0
-rw-r--r--spec/frontend/test_setup.js56
-rw-r--r--spec/frontend/u2f/util_spec.js (renamed from spec/javascripts/u2f/util_spec.js)0
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js (renamed from spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js)0
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js (renamed from spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js)0
-rw-r--r--spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js (renamed from spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js)0
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js (renamed from spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js)0
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commits_header_spec.js (renamed from spec/javascripts/vue_mr_widget/components/states/mr_widget_commits_header_spec.js)26
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js (renamed from spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap21
-rw-r--r--spec/frontend/vue_shared/components/callout_spec.js (renamed from spec/javascripts/vue_shared/components/callout_spec.js)0
-rw-r--r--spec/frontend/vue_shared/components/code_block_spec.js (renamed from spec/javascripts/vue_shared/components/code_block_spec.js)0
-rw-r--r--spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js (renamed from spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js)0
-rw-r--r--spec/frontend/vue_shared/components/identicon_spec.js (renamed from spec/javascripts/vue_shared/components/identicon_spec.js)0
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js (renamed from spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js172
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_warning_spec.js (renamed from spec/javascripts/vue_shared/components/issue/issue_warning_spec.js)28
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js198
-rw-r--r--spec/frontend/vue_shared/components/lib/utils/dom_utils_spec.js (renamed from spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js)0
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js103
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js98
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js40
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js (renamed from spec/javascripts/vue_shared/components/notes/system_note_spec.js)7
-rw-r--r--spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/pagination_links_spec.js (renamed from spec/javascripts/vue_shared/components/pagination_links_spec.js)0
-rw-r--r--spec/frontend/vue_shared/components/resizable_chart_container_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/date_picker_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js)8
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js)48
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js)23
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js)5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js)5
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js)2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js)31
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js)48
-rw-r--r--spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/time_ago_tooltip_spec.js (renamed from spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js)0
-rw-r--r--spec/frontend/vue_shared/droplab_dropdown_button_spec.js136
-rw-r--r--spec/frontend/vuex_shared/modules/modal/mutations_spec.js (renamed from spec/javascripts/vuex_shared/modules/modal/mutations_spec.js)6
-rw-r--r--spec/graphql/features/authorization_spec.rb335
-rw-r--r--spec/graphql/gitlab_schema_spec.rb136
-rw-r--r--spec/graphql/resolvers/base_resolver_spec.rb17
-rw-r--r--spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb8
-rw-r--r--spec/graphql/resolvers/group_resolver_spec.rb32
-rw-r--r--spec/graphql/resolvers/issues_resolver_spec.rb160
-rw-r--r--spec/graphql/resolvers/metadata_resolver_spec.rb11
-rw-r--r--spec/graphql/resolvers/namespace_projects_resolver_spec.rb69
-rw-r--r--spec/graphql/resolvers/project_resolver_spec.rb8
-rw-r--r--spec/graphql/resolvers/tree_resolver_spec.rb35
-rw-r--r--spec/graphql/types/base_field_spec.rb56
-rw-r--r--spec/graphql/types/ci/detailed_status_type_spec.rb11
-rw-r--r--spec/graphql/types/group_type_spec.rb11
-rw-r--r--spec/graphql/types/issue_type_spec.rb8
-rw-r--r--spec/graphql/types/merge_request_type_spec.rb11
-rw-r--r--spec/graphql/types/metadata_type_spec.rb5
-rw-r--r--spec/graphql/types/milestone_type_spec.rb9
-rw-r--r--spec/graphql/types/namespace_type_spec.rb9
-rw-r--r--spec/graphql/types/permission_types/issue_spec.rb4
-rw-r--r--spec/graphql/types/permission_types/project_spec.rb4
-rw-r--r--spec/graphql/types/project_statistics_type_spec.rb11
-rw-r--r--spec/graphql/types/project_type_spec.rb16
-rw-r--r--spec/graphql/types/query_type_spec.rb25
-rw-r--r--spec/graphql/types/repository_type_spec.rb11
-rw-r--r--spec/graphql/types/tree/blob_type_spec.rb9
-rw-r--r--spec/graphql/types/tree/submodule_type_spec.rb9
-rw-r--r--spec/graphql/types/tree/tree_entry_type_spec.rb9
-rw-r--r--spec/graphql/types/tree/tree_type_spec.rb9
-rw-r--r--spec/graphql/types/tree/type_enum_spec.rb11
-rw-r--r--spec/graphql/types/user_type_spec.rb9
-rw-r--r--spec/haml_lint/linter/no_plain_nodes_spec.rb56
-rw-r--r--spec/helpers/appearances_helper_spec.rb8
-rw-r--r--spec/helpers/auth_helper_spec.rb40
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb138
-rw-r--r--spec/helpers/blob_helper_spec.rb21
-rw-r--r--spec/helpers/clusters_helper_spec.rb33
-rw-r--r--spec/helpers/dashboard_helper_spec.rb6
-rw-r--r--spec/helpers/emails_helper_spec.rb93
-rw-r--r--spec/helpers/environments_helper_spec.rb49
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb2
-rw-r--r--spec/helpers/groups/group_members_helper_spec.rb17
-rw-r--r--spec/helpers/groups_helper_spec.rb35
-rw-r--r--spec/helpers/icons_helper_spec.rb6
-rw-r--r--spec/helpers/issuables_helper_spec.rb6
-rw-r--r--spec/helpers/labels_helper_spec.rb87
-rw-r--r--spec/helpers/markup_helper_spec.rb3
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb2
-rw-r--r--spec/helpers/namespaces_helper_spec.rb72
-rw-r--r--spec/helpers/nav_helper_spec.rb12
-rw-r--r--spec/helpers/page_layout_helper_spec.rb8
-rw-r--r--spec/helpers/preferences_helper_spec.rb16
-rw-r--r--spec/helpers/projects_helper_spec.rb98
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/helpers/storage_helper_spec.rb25
-rw-r--r--spec/helpers/tracking_helper_spec.rb11
-rw-r--r--spec/helpers/version_check_helper_spec.rb8
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb45
-rw-r--r--spec/helpers/wiki_helper_spec.rb52
-rw-r--r--spec/initializers/secret_token_spec.rb4
-rw-r--r--spec/javascripts/ajax_loading_spinner_spec.js2
-rw-r--r--spec/javascripts/awards_handler_spec.js4
-rw-r--r--spec/javascripts/badges/components/badge_list_spec.js2
-rw-r--r--spec/javascripts/badges/components/badge_spec.js2
-rw-r--r--spec/javascripts/badges/store/actions_spec.js4
-rw-r--r--spec/javascripts/behaviors/copy_as_gfm_spec.js4
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js4
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js4
-rw-r--r--spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js2
-rw-r--r--spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js8
-rw-r--r--spec/javascripts/blob/blob_file_dropzone_spec.js4
-rw-r--r--spec/javascripts/blob/notebook/index_spec.js4
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js8
-rw-r--r--spec/javascripts/blob/sketch/index_spec.js4
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js4
-rw-r--r--spec/javascripts/boards/board_card_spec.js4
-rw-r--r--spec/javascripts/boards/board_list_common_spec.js58
-rw-r--r--spec/javascripts/boards/board_list_spec.js53
-rw-r--r--spec/javascripts/boards/boards_store_spec.js91
-rw-r--r--spec/javascripts/boards/components/board_spec.js16
-rw-r--r--spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js43
-rw-r--r--spec/javascripts/boards/components/issue_due_date_spec.js4
-rw-r--r--spec/javascripts/boards/issue_card_spec.js10
-rw-r--r--spec/javascripts/boards/issue_spec.js6
-rw-r--r--spec/javascripts/boards/list_spec.js7
-rw-r--r--spec/javascripts/boards/mock_data.js9
-rw-r--r--spec/javascripts/bootstrap_linked_tabs_spec.js4
-rw-r--r--spec/javascripts/breakpoints_spec.js14
-rw-r--r--spec/javascripts/ci_variable_list/ajax_variable_list_spec.js18
-rw-r--r--spec/javascripts/ci_variable_list/ci_variable_list_spec.js94
-rw-r--r--spec/javascripts/ci_variable_list/native_form_variable_list_spec.js4
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js268
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js2
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/create_item_dropdown_spec.js4
-rw-r--r--spec/javascripts/diffs/components/app_spec.js405
-rw-r--r--spec/javascripts/diffs/components/changed_files_dropdown_spec.js1
-rw-r--r--spec/javascripts/diffs/components/commit_item_spec.js6
-rw-r--r--spec/javascripts/diffs/components/compare_versions_dropdown_spec.js153
-rw-r--r--spec/javascripts/diffs/components/compare_versions_spec.js20
-rw-r--r--spec/javascripts/diffs/components/diff_content_spec.js16
-rw-r--r--spec/javascripts/diffs/components/diff_discussions_spec.js111
-rw-r--r--spec/javascripts/diffs/components/diff_file_header_spec.js222
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js35
-rw-r--r--spec/javascripts/diffs/components/edit_button_spec.js1
-rw-r--r--spec/javascripts/diffs/components/hidden_files_warning_spec.js1
-rw-r--r--spec/javascripts/diffs/components/inline_diff_view_spec.js2
-rw-r--r--spec/javascripts/diffs/components/parallel_diff_view_spec.js6
-rw-r--r--spec/javascripts/diffs/mock_data/diff_discussions.js3
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js1
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js264
-rw-r--r--spec/javascripts/diffs/store/getters_spec.js20
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js174
-rw-r--r--spec/javascripts/diffs/store/utils_spec.js45
-rw-r--r--spec/javascripts/dirty_submit/dirty_submit_form_spec.js111
-rw-r--r--spec/javascripts/environments/confirm_rollback_modal_spec.js70
-rw-r--r--spec/javascripts/environments/environment_item_spec.js20
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js32
-rw-r--r--spec/javascripts/environments/environment_table_spec.js234
-rw-r--r--spec/javascripts/environments/environments_app_spec.js7
-rw-r--r--spec/javascripts/environments/environments_store_spec.js74
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js16
-rw-r--r--spec/javascripts/error_tracking_settings/components/app_spec.js63
-rw-r--r--spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js91
-rw-r--r--spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js109
-rw-r--r--spec/javascripts/error_tracking_settings/mock.js92
-rw-r--r--spec/javascripts/error_tracking_settings/store/actions_spec.js191
-rw-r--r--spec/javascripts/error_tracking_settings/store/getters_spec.js93
-rw-r--r--spec/javascripts/error_tracking_settings/store/mutation_spec.js82
-rw-r--r--spec/javascripts/error_tracking_settings/utils_spec.js29
-rw-r--r--spec/javascripts/filtered_search/dropdown_user_spec.js2
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js2
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js339
-rw-r--r--spec/javascripts/filtered_search/visual_token_value_spec.js383
-rw-r--r--spec/javascripts/fixtures/.gitignore3
-rw-r--r--spec/javascripts/fixtures/abuse_reports.rb3
-rw-r--r--spec/javascripts/fixtures/admin_users.rb3
-rw-r--r--spec/javascripts/fixtures/ajax_loading_spinner.html.haml2
-rw-r--r--spec/javascripts/fixtures/application_settings.rb3
-rw-r--r--spec/javascripts/fixtures/autocomplete_sources.rb39
-rw-r--r--spec/javascripts/fixtures/balsamiq.rb18
-rw-r--r--spec/javascripts/fixtures/balsamiq_viewer.html.haml1
-rw-r--r--spec/javascripts/fixtures/blob.rb3
-rw-r--r--spec/javascripts/fixtures/boards.rb3
-rw-r--r--spec/javascripts/fixtures/branches.rb3
-rw-r--r--spec/javascripts/fixtures/clusters.rb3
-rw-r--r--spec/javascripts/fixtures/commit.rb3
-rw-r--r--spec/javascripts/fixtures/create_item_dropdown.html.haml13
-rw-r--r--spec/javascripts/fixtures/deploy_keys.rb3
-rw-r--r--spec/javascripts/fixtures/environments/table.html.haml11
-rw-r--r--spec/javascripts/fixtures/event_filter.html.haml25
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml17
-rw-r--r--spec/javascripts/fixtures/gl_field_errors.html.haml15
-rw-r--r--spec/javascripts/fixtures/groups.rb6
-rw-r--r--spec/javascripts/fixtures/issuable_filter.html.haml8
-rw-r--r--spec/javascripts/fixtures/issue_sidebar_label.html.haml16
-rw-r--r--spec/javascripts/fixtures/issues.rb79
-rw-r--r--spec/javascripts/fixtures/jobs.rb6
-rw-r--r--spec/javascripts/fixtures/labels.rb6
-rw-r--r--spec/javascripts/fixtures/line_highlighter.html.haml11
-rw-r--r--spec/javascripts/fixtures/linked_tabs.html.haml13
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb43
-rw-r--r--spec/javascripts/fixtures/merge_requests_diffs.rb15
-rw-r--r--spec/javascripts/fixtures/merge_requests_show.html.haml13
-rw-r--r--spec/javascripts/fixtures/mini_dropdown_graph.html.haml10
-rw-r--r--spec/javascripts/fixtures/notebook_viewer.html.haml1
-rw-r--r--spec/javascripts/fixtures/oauth_remember_me.html.haml6
-rw-r--r--spec/javascripts/fixtures/pdf.rb18
-rw-r--r--spec/javascripts/fixtures/pdf_viewer.html.haml1
-rw-r--r--spec/javascripts/fixtures/pipeline_graph.html.haml14
-rw-r--r--spec/javascripts/fixtures/pipeline_schedules.rb6
-rw-r--r--spec/javascripts/fixtures/pipelines.html.haml12
-rw-r--r--spec/javascripts/fixtures/pipelines.rb3
-rw-r--r--spec/javascripts/fixtures/project_select_combo_button.html.haml6
-rw-r--r--spec/javascripts/fixtures/projects.rb15
-rw-r--r--spec/javascripts/fixtures/prometheus_service.rb3
-rw-r--r--spec/javascripts/fixtures/raw.rb27
-rw-r--r--spec/javascripts/fixtures/search.rb3
-rw-r--r--spec/javascripts/fixtures/search_autocomplete.html.haml9
-rw-r--r--spec/javascripts/fixtures/services.rb3
-rw-r--r--spec/javascripts/fixtures/sessions.rb3
-rw-r--r--spec/javascripts/fixtures/signin_tabs.html.haml5
-rw-r--r--spec/javascripts/fixtures/sketch_viewer.html.haml2
-rw-r--r--spec/javascripts/fixtures/snippet.rb3
-rw-r--r--spec/javascripts/fixtures/static/README.md3
-rw-r--r--spec/javascripts/fixtures/static/ajax_loading_spinner.html3
-rw-r--r--spec/javascripts/fixtures/static/balsamiq_viewer.html1
-rw-r--r--spec/javascripts/fixtures/static/create_item_dropdown.html11
-rw-r--r--spec/javascripts/fixtures/static/environments/table.html15
-rw-r--r--spec/javascripts/fixtures/static/event_filter.html44
-rw-r--r--spec/javascripts/fixtures/static/gl_dropdown.html26
-rw-r--r--spec/javascripts/fixtures/static/gl_field_errors.html22
-rw-r--r--spec/javascripts/fixtures/static/images/green_box.png (renamed from spec/javascripts/fixtures/images/green_box.png)bin1306 -> 1306 bytes
-rw-r--r--spec/javascripts/fixtures/static/images/one_white_pixel.png (renamed from spec/javascripts/fixtures/one_white_pixel.png)bin68 -> 68 bytes
-rw-r--r--spec/javascripts/fixtures/static/images/red_box.png (renamed from spec/javascripts/fixtures/images/red_box.png)bin1305 -> 1305 bytes
-rw-r--r--spec/javascripts/fixtures/static/issuable_filter.html9
-rw-r--r--spec/javascripts/fixtures/static/issue_sidebar_label.html26
-rw-r--r--spec/javascripts/fixtures/static/line_highlighter.html107
-rw-r--r--spec/javascripts/fixtures/static/linked_tabs.html20
-rw-r--r--spec/javascripts/fixtures/static/merge_requests_show.html15
-rw-r--r--spec/javascripts/fixtures/static/mini_dropdown_graph.html13
-rw-r--r--spec/javascripts/fixtures/static/notebook_viewer.html1
-rw-r--r--spec/javascripts/fixtures/static/oauth_remember_me.html6
-rw-r--r--spec/javascripts/fixtures/static/pdf_viewer.html1
-rw-r--r--spec/javascripts/fixtures/static/pipeline_graph.html24
-rw-r--r--spec/javascripts/fixtures/static/pipelines.html3
-rw-r--r--spec/javascripts/fixtures/static/project_select_combo_button.html9
-rw-r--r--spec/javascripts/fixtures/static/projects.json (renamed from spec/javascripts/fixtures/projects.json)0
-rw-r--r--spec/javascripts/fixtures/static/search_autocomplete.html15
-rw-r--r--spec/javascripts/fixtures/static/signin_tabs.html8
-rw-r--r--spec/javascripts/fixtures/static/sketch_viewer.html3
-rw-r--r--spec/javascripts/fixtures/static_fixtures.rb31
-rw-r--r--spec/javascripts/fixtures/todos.rb6
-rw-r--r--spec/javascripts/fixtures/u2f.rb6
-rw-r--r--spec/javascripts/fly_out_nav_spec.js5
-rw-r--r--spec/javascripts/frequent_items/components/app_spec.js2
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js62
-rw-r--r--spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js25
-rw-r--r--spec/javascripts/gl_dropdown_spec.js8
-rw-r--r--spec/javascripts/gl_field_errors_spec.js4
-rw-r--r--spec/javascripts/groups/components/app_spec.js2
-rw-r--r--spec/javascripts/header_spec.js2
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js2
-rw-r--r--spec/javascripts/helpers/text_helper.js18
-rw-r--r--spec/javascripts/helpers/vue_test_utils_helper.js4
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/actions_spec.js41
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js2
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js73
-rw-r--r--spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js7
-rw-r--r--spec/javascripts/ide/components/error_message_spec.js2
-rw-r--r--spec/javascripts/ide/components/file_row_extra_spec.js2
-rw-r--r--spec/javascripts/ide/components/ide_review_spec.js2
-rw-r--r--spec/javascripts/ide/components/ide_spec.js92
-rw-r--r--spec/javascripts/ide/components/ide_tree_list_spec.js59
-rw-r--r--spec/javascripts/ide/components/nav_dropdown_button_spec.js2
-rw-r--r--spec/javascripts/ide/components/new_dropdown/index_spec.js4
-rw-r--r--spec/javascripts/ide/components/new_dropdown/modal_spec.js67
-rw-r--r--spec/javascripts/ide/components/new_dropdown/upload_spec.js4
-rw-r--r--spec/javascripts/ide/components/pipelines/list_spec.js31
-rw-r--r--spec/javascripts/ide/lib/diff/diff_spec.js80
-rw-r--r--spec/javascripts/ide/mock_data.js1
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js109
-rw-r--r--spec/javascripts/ide/stores/actions/merge_request_spec.js142
-rw-r--r--spec/javascripts/ide/stores/actions/project_spec.js189
-rw-r--r--spec/javascripts/ide/stores/actions/tree_spec.js72
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js97
-rw-r--r--spec/javascripts/ide/stores/getters_spec.js32
-rw-r--r--spec/javascripts/ide/stores/modules/commit/actions_spec.js243
-rw-r--r--spec/javascripts/ide/stores/modules/commit/getters_spec.js37
-rw-r--r--spec/javascripts/ide/stores/modules/file_templates/actions_spec.js54
-rw-r--r--spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js69
-rw-r--r--spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js106
-rw-r--r--spec/javascripts/ide/stores/mutations/branch_spec.js40
-rw-r--r--spec/javascripts/ide/stores/mutations/tree_spec.js61
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js14
-rw-r--r--spec/javascripts/ide/stores/utils_spec.js144
-rw-r--r--spec/javascripts/import_projects/components/import_projects_table_spec.js186
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js2
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js51
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js4
-rw-r--r--spec/javascripts/issue_show/components/fields/description_spec.js4
-rw-r--r--spec/javascripts/issue_show/components/fields/title_spec.js4
-rw-r--r--spec/javascripts/issue_show/components/form_spec.js25
-rw-r--r--spec/javascripts/issue_spec.js11
-rw-r--r--spec/javascripts/jobs/components/artifacts_block_spec.js2
-rw-r--r--spec/javascripts/jobs/components/commit_block_spec.js3
-rw-r--r--spec/javascripts/jobs/components/job_app_spec.js41
-rw-r--r--spec/javascripts/jobs/components/sidebar_spec.js16
-rw-r--r--spec/javascripts/jobs/components/stages_dropdown_spec.js189
-rw-r--r--spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js37
-rw-r--r--spec/javascripts/jobs/mock_data.js302
-rw-r--r--spec/javascripts/jobs/store/actions_spec.js105
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js4
-rw-r--r--spec/javascripts/labels_select_spec.js52
-rw-r--r--spec/javascripts/lazy_loader_spec.js6
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js59
-rw-r--r--spec/javascripts/lib/utils/higlight_spec.js43
-rw-r--r--spec/javascripts/line_highlighter_spec.js4
-rw-r--r--spec/javascripts/matchers.js2
-rw-r--r--spec/javascripts/merge_request_spec.js10
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js6
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js4
-rw-r--r--spec/javascripts/monitoring/charts/area_spec.js93
-rw-r--r--spec/javascripts/monitoring/charts/single_stat_spec.js28
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js316
-rw-r--r--spec/javascripts/monitoring/helpers.js8
-rw-r--r--spec/javascripts/monitoring/mock_data.js5876
-rw-r--r--spec/javascripts/monitoring/monitoring_store_spec.js35
-rw-r--r--spec/javascripts/monitoring/store/actions_spec.js158
-rw-r--r--spec/javascripts/monitoring/store/mutations_spec.js92
-rw-r--r--spec/javascripts/monitoring/utils_spec.js29
-rw-r--r--spec/javascripts/new_branch_spec.js4
-rw-r--r--spec/javascripts/notes/components/discussion_filter_note_spec.js93
-rw-r--r--spec/javascripts/notes/components/discussion_filter_spec.js22
-rw-r--r--spec/javascripts/notes/components/note_actions_spec.js85
-rw-r--r--spec/javascripts/notes/components/note_form_spec.js241
-rw-r--r--spec/javascripts/notes/components/noteable_discussion_spec.js37
-rw-r--r--spec/javascripts/notes/mock_data.js6
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js175
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js8
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js17
-rw-r--r--spec/javascripts/oauth_remember_me_spec.js4
-rw-r--r--spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js2
-rw-r--r--spec/javascripts/pages/admin/users/new/index_spec.js2
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js244
-rw-r--r--spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js4
-rw-r--r--spec/javascripts/pdf/index_spec.js10
-rw-r--r--spec/javascripts/pdf/page_spec.js10
-rw-r--r--spec/javascripts/performance_bar/components/detailed_metric_spec.js22
-rw-r--r--spec/javascripts/persistent_user_callout_spec.js88
-rw-r--r--spec/javascripts/pipelines/graph/action_component_spec.js28
-rw-r--r--spec/javascripts/pipelines/graph/stage_column_component_spec.js53
-rw-r--r--spec/javascripts/pipelines/mock_data.js1
-rw-r--r--spec/javascripts/pipelines/pipeline_triggerer_spec.js54
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js65
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js8
-rw-r--r--spec/javascripts/pipelines/stage_spec.js16
-rw-r--r--spec/javascripts/pipelines_spec.js4
-rw-r--r--spec/javascripts/project_select_combo_button_spec.js2
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js44
-rw-r--r--spec/javascripts/projects/project_new_spec.js14
-rw-r--r--spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js2
-rw-r--r--spec/javascripts/raven/index_spec.js10
-rw-r--r--spec/javascripts/raven/raven_config_spec.js10
-rw-r--r--spec/javascripts/read_more_spec.js2
-rw-r--r--spec/javascripts/registry/components/app_spec.js2
-rw-r--r--spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js89
-rw-r--r--spec/javascripts/related_merge_requests/store/actions_spec.js110
-rw-r--r--spec/javascripts/related_merge_requests/store/mutations_spec.js49
-rw-r--r--spec/javascripts/reports/components/grouped_test_reports_app_spec.js10
-rw-r--r--spec/javascripts/reports/components/modal_spec.js2
-rw-r--r--spec/javascripts/reports/components/test_issue_body_spec.js2
-rw-r--r--spec/javascripts/right_sidebar_spec.js2
-rw-r--r--spec/javascripts/search_autocomplete_spec.js4
-rw-r--r--spec/javascripts/search_spec.js2
-rw-r--r--spec/javascripts/serverless/components/function_row_spec.js33
-rw-r--r--spec/javascripts/serverless/components/functions_spec.js68
-rw-r--r--spec/javascripts/serverless/mock_data.js79
-rw-r--r--spec/javascripts/serverless/stores/serverless_store_spec.js36
-rw-r--r--spec/javascripts/settings_panels_spec.js4
-rw-r--r--spec/javascripts/shortcuts_spec.js2
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js108
-rw-r--r--spec/javascripts/sidebar/sidebar_assignees_spec.js4
-rw-r--r--spec/javascripts/sidebar/todo_spec.js6
-rw-r--r--spec/javascripts/signin_tabs_memoizer_spec.js2
-rw-r--r--spec/javascripts/test_bundle.js75
-rw-r--r--spec/javascripts/test_constants.js10
-rw-r--r--spec/javascripts/todos_spec.js4
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js4
-rw-r--r--spec/javascripts/u2f/register_spec.js4
-rw-r--r--spec/javascripts/user_popovers_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js77
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js32
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js102
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js4
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js189
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js54
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js99
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js90
-rw-r--r--spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js3
-rw-r--r--spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js16
-rw-r--r--spec/javascripts/vue_shared/components/file_icon_spec.js7
-rw-r--r--spec/javascripts/vue_shared/components/file_row_spec.js60
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js234
-rw-r--r--spec/javascripts/vue_shared/components/issue/related_issuable_mock_data.js111
-rw-r--r--spec/javascripts/vue_shared/components/markdown/field_spec.js8
-rw-r--r--spec/javascripts/vue_shared/components/markdown/header_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js75
-rw-r--r--spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js66
-rw-r--r--spec/javascripts/vue_shared/components/markdown/suggestions_spec.js109
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js110
-rw-r--r--spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js132
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js7
-rw-r--r--spec/javascripts/vue_shared/components/table_pagination_spec.js194
-rw-r--r--spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js20
-rw-r--r--spec/javascripts/vue_shared/translate_spec.js2
-rw-r--r--spec/javascripts/zen_mode_spec.js2
-rw-r--r--spec/lib/api/entities/job_request/image_spec.rb31
-rw-r--r--spec/lib/api/entities/job_request/port_spec.rb22
-rw-r--r--spec/lib/api/helpers/custom_validators_spec.rb27
-rw-r--r--spec/lib/api/helpers/pagination_spec.rb34
-rw-r--r--spec/lib/api/helpers/related_resources_helpers_spec.rb34
-rw-r--r--spec/lib/api/helpers_spec.rb12
-rw-r--r--spec/lib/backup/uploads_spec.rb18
-rw-r--r--spec/lib/banzai/commit_renderer_spec.rb4
-rw-r--r--spec/lib/banzai/filter/blockquote_fence_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb43
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb25
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb21
-rw-r--r--spec/lib/banzai/filter/output_safety_spec.rb29
-rw-r--r--spec/lib/banzai/filter/plantuml_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/suggestion_filter_spec.rb14
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb32
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb5
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb42
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb28
-rw-r--r--spec/lib/banzai/redactor_spec.rb32
-rw-r--r--spec/lib/banzai/renderer_spec.rb14
-rw-r--r--spec/lib/banzai/suggestions_parser_spec.rb32
-rw-r--r--spec/lib/constraints/project_url_constrainer_spec.rb4
-rw-r--r--spec/lib/event_filter_spec.rb8
-rw-r--r--spec/lib/extracts_path_spec.rb30
-rw-r--r--spec/lib/forever_spec.rb4
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb153
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb11
-rw-r--r--spec/lib/gitlab/auth_spec.rb75
-rw-r--r--spec/lib/gitlab/authorized_keys_spec.rb194
-rw-r--r--spec/lib/gitlab/background_migration/delete_diff_files_spec.rb6
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_legacy_uploads_spec.rb253
-rw-r--r--spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb3
-rw-r--r--spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb56
-rw-r--r--spec/lib/gitlab/background_migration/reset_merge_status_spec.rb48
-rw-r--r--spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb62
-rw-r--r--spec/lib/gitlab/background_migration_spec.rb40
-rw-r--r--spec/lib/gitlab/badge/pipeline/template_spec.rb10
-rw-r--r--spec/lib/gitlab/bitbucket_import/importer_spec.rb61
-rw-r--r--spec/lib/gitlab/bitbucket_server_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/checks/branch_check_spec.rb114
-rw-r--r--spec/lib/gitlab/checks/lfs_check_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/build/image_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/build/policy/changes_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/build/policy/variables_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/build/port_spec.rb27
-rw-r--r--spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb97
-rw-r--r--spec/lib/gitlab/ci/config/entry/environment_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/entry/global_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/image_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/entry/port_spec.rb173
-rw-r--r--spec/lib/gitlab/ci/config/entry/ports_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/config/entry/service_spec.rb80
-rw-r--r--spec/lib/gitlab/ci/config/entry/services_spec.rb87
-rw-r--r--spec/lib/gitlab/ci/config/extendable/entry_spec.rb46
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config/external/file/local_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/external/file/project_spec.rb69
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb58
-rw-r--r--spec/lib/gitlab/ci/config/external/file/template_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb140
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/cron_parser_spec.rb7
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/build_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/command_spec.rb66
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb77
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb55
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb120
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb73
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb122
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb77
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb96
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb50
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb225
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb37
-rw-r--r--spec/lib/gitlab/ci/status/build/preparing_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/status/preparing_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/status/stage/factory_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/status/stage/play_manual_spec.rb74
-rw-r--r--spec/lib/gitlab/ci/status/success_warning_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/templates_spec.rb29
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/variables/collection/item_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb12
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb102
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb4
-rw-r--r--spec/lib/gitlab/correlation_id_spec.rb77
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb17
-rw-r--r--spec/lib/gitlab/danger/helper_spec.rb119
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb101
-rw-r--r--spec/lib/gitlab/danger/teammate_spec.rb70
-rw-r--r--spec/lib/gitlab/data_builder/deployment_spec.rb39
-rw-r--r--spec/lib/gitlab/data_builder/pipeline_spec.rb9
-rw-r--r--spec/lib/gitlab/data_builder/push_spec.rb11
-rw-r--r--spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb19
-rw-r--r--spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb16
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb4
-rw-r--r--spec/lib/gitlab/database_spec.rb52
-rw-r--r--spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb9
-rw-r--r--spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb18
-rw-r--r--spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb44
-rw-r--r--spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb5
-rw-r--r--spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb4
-rw-r--r--spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb208
-rw-r--r--spec/lib/gitlab/diff/suggestion_diff_spec.rb55
-rw-r--r--spec/lib/gitlab/diff/suggestion_spec.rb153
-rw-r--r--spec/lib/gitlab/diff/suggestions_parser_spec.rb134
-rw-r--r--spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb110
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb13
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb18
-rw-r--r--spec/lib/gitlab/external_authorization/access_spec.rb142
-rw-r--r--spec/lib/gitlab/external_authorization/cache_spec.rb48
-rw-r--r--spec/lib/gitlab/external_authorization/client_spec.rb97
-rw-r--r--spec/lib/gitlab/external_authorization/logger_spec.rb45
-rw-r--r--spec/lib/gitlab/external_authorization/response_spec.rb52
-rw-r--r--spec/lib/gitlab/external_authorization_spec.rb54
-rw-r--r--spec/lib/gitlab/fake_application_settings_spec.rb31
-rw-r--r--spec/lib/gitlab/favicon_spec.rb2
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb18
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb79
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb8
-rw-r--r--spec/lib/gitlab/git/gitmodules_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/git/object_pool_spec.rb41
-rw-r--r--spec/lib/gitlab/git/pre_receive_error_spec.rb16
-rw-r--r--spec/lib/gitlab/git/repository_cleaner_spec.rb73
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb337
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb76
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb16
-rw-r--r--spec/lib/gitlab/git_ref_validator_spec.rb92
-rw-r--r--spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb15
-rw-r--r--spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb20
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb21
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb41
-rw-r--r--spec/lib/gitlab/gitaly_client/storage_settings_spec.rb10
-rw-r--r--spec/lib/gitlab/gitaly_client/wiki_service_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb80
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb24
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb78
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb16
-rw-r--r--spec/lib/gitlab/github_import/parallel_importer_spec.rb11
-rw-r--r--spec/lib/gitlab/gl_repository/repo_type_spec.rb64
-rw-r--r--spec/lib/gitlab/gl_repository_spec.rb4
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb132
-rw-r--r--spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb18
-rw-r--r--spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb67
-rw-r--r--spec/lib/gitlab/graphql/authorize_spec.rb20
-rw-r--r--spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb5
-rw-r--r--spec/lib/gitlab/graphql/generic_tracing_spec.rb67
-rw-r--r--spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb18
-rw-r--r--spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb25
-rw-r--r--spec/lib/gitlab/graphql/representation/tree_entry_spec.rb20
-rw-r--r--spec/lib/gitlab/graphql_logger_spec.rb40
-rw-r--r--spec/lib/gitlab/group_search_results_spec.rb69
-rw-r--r--spec/lib/gitlab/hashed_storage/migrator_spec.rb166
-rw-r--r--spec/lib/gitlab/hook_data/issuable_builder_spec.rb6
-rw-r--r--spec/lib/gitlab/hook_data/merge_request_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/http_connection_adapter_spec.rb120
-rw-r--r--spec/lib/gitlab/http_spec.rb28
-rw-r--r--spec/lib/gitlab/import/merge_request_helpers_spec.rb73
-rw-r--r--spec/lib/gitlab/import/set_async_jid_spec.rb23
-rw-r--r--spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb14
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml17
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/members_mapper_spec.rb20
-rw-r--r--spec/lib/gitlab/import_export/merge_request_parser_spec.rb16
-rw-r--r--spec/lib/gitlab/import_export/project.json46
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb33
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/repo_restorer_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml27
-rw-r--r--spec/lib/gitlab/import_sources_spec.rb13
-rw-r--r--spec/lib/gitlab/issuable_metadata_spec.rb12
-rw-r--r--spec/lib/gitlab/json_cache_spec.rb82
-rw-r--r--spec/lib/gitlab/json_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/config_map_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb46
-rw-r--r--spec/lib/gitlab/kubernetes/helm/base_command_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/helm/certificate_spec.rb6
-rw-r--r--spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb72
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb20
-rw-r--r--spec/lib/gitlab/kubernetes/kube_client_spec.rb30
-rw-r--r--spec/lib/gitlab/kubernetes/namespace_spec.rb27
-rw-r--r--spec/lib/gitlab/kubernetes/role_binding_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/service_account_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes/service_account_token_spec.rb2
-rw-r--r--spec/lib/gitlab/kubernetes_spec.rb34
-rw-r--r--spec/lib/gitlab/legacy_github_import/project_creator_spec.rb16
-rw-r--r--spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb1
-rw-r--r--spec/lib/gitlab/lets_encrypt/challenge_spec.rb17
-rw-r--r--spec/lib/gitlab/lets_encrypt/client_spec.rb162
-rw-r--r--spec/lib/gitlab/lets_encrypt/order_spec.rb41
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb98
-rw-r--r--spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb179
-rw-r--r--spec/lib/gitlab/markdown_cache/field_data_spec.rb15
-rw-r--r--spec/lib/gitlab/markdown_cache/redis/extension_spec.rb76
-rw-r--r--spec/lib/gitlab/markdown_cache/redis/store_spec.rb68
-rw-r--r--spec/lib/gitlab/metrics/dashboard/finder_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/processor_spec.rb105
-rw-r--r--spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb32
-rw-r--r--spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb123
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb24
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb229
-rw-r--r--spec/lib/gitlab/middleware/basic_health_check_spec.rb29
-rw-r--r--spec/lib/gitlab/namespaced_session_store_spec.rb22
-rw-r--r--spec/lib/gitlab/object_hierarchy_spec.rb40
-rw-r--r--spec/lib/gitlab/omniauth_initializer_spec.rb30
-rw-r--r--spec/lib/gitlab/optimistic_locking_spec.rb4
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb12
-rw-r--r--spec/lib/gitlab/phabricator_import/base_worker_spec.rb74
-rw-r--r--spec/lib/gitlab/phabricator_import/cache/map_spec.rb66
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/client_spec.rb59
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb39
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/response_spec.rb79
-rw-r--r--spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb27
-rw-r--r--spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb16
-rw-r--r--spec/lib/gitlab/phabricator_import/importer_spec.rb32
-rw-r--r--spec/lib/gitlab/phabricator_import/issues/importer_spec.rb53
-rw-r--r--spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb54
-rw-r--r--spec/lib/gitlab/phabricator_import/project_creator_spec.rb58
-rw-r--r--spec/lib/gitlab/phabricator_import/representation/task_spec.rb33
-rw-r--r--spec/lib/gitlab/phabricator_import/worker_state_spec.rb46
-rw-r--r--spec/lib/gitlab/profiler_spec.rb14
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb32
-rw-r--r--spec/lib/gitlab/project_template_spec.rb5
-rw-r--r--spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb30
-rw-r--r--spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb26
-rw-r--r--spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb4
-rw-r--r--spec/lib/gitlab/prometheus/query_variables_spec.rb28
-rw-r--r--spec/lib/gitlab/prometheus_client_spec.rb107
-rw-r--r--spec/lib/gitlab/push_options_spec.rb103
-rw-r--r--spec/lib/gitlab/quick_actions/command_definition_spec.rb37
-rw-r--r--spec/lib/gitlab/quick_actions/dsl_spec.rb26
-rw-r--r--spec/lib/gitlab/rack_timeout_observer_spec.rb58
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb2
-rw-r--r--spec/lib/gitlab/repo_path_spec.rb22
-rw-r--r--spec/lib/gitlab/request_context_spec.rb27
-rw-r--r--spec/lib/gitlab/route_map_spec.rb2
-rw-r--r--spec/lib/gitlab/sanitizers/exif_spec.rb120
-rw-r--r--spec/lib/gitlab/search_results_spec.rb44
-rw-r--r--spec/lib/gitlab/sentry_spec.rb9
-rw-r--r--spec/lib/gitlab/session_spec.rb27
-rw-r--r--spec/lib/gitlab/shell_spec.rb595
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb2
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb74
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb88
-rw-r--r--spec/lib/gitlab/sidekiq_signals_spec.rb69
-rw-r--r--spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb7
-rw-r--r--spec/lib/gitlab/tracing/factory_spec.rb43
-rw-r--r--spec/lib/gitlab/tracing/grpc_interceptor_spec.rb47
-rw-r--r--spec/lib/gitlab/tracing/jaeger_factory_spec.rb71
-rw-r--r--spec/lib/gitlab/tracing/rack_middleware_spec.rb62
-rw-r--r--spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb147
-rw-r--r--spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb73
-rw-r--r--spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb43
-rw-r--r--spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb43
-rw-r--r--spec/lib/gitlab/tracing_spec.rb6
-rw-r--r--spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb118
-rw-r--r--spec/lib/gitlab/untrusted_regexp_spec.rb74
-rw-r--r--spec/lib/gitlab/url_blocker_spec.rb89
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/url_sanitizer_spec.rb36
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb29
-rw-r--r--spec/lib/gitlab/user_extractor_spec.rb20
-rw-r--r--spec/lib/gitlab/utils_spec.rb18
-rw-r--r--spec/lib/gitlab/visibility_level_spec.rb8
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb102
-rw-r--r--spec/lib/gitlab_spec.rb46
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb12
-rw-r--r--spec/lib/mattermost/session_spec.rb7
-rw-r--r--spec/lib/object_storage/direct_upload_spec.rb2
-rw-r--r--spec/lib/quality/kubernetes_client_spec.rb4
-rw-r--r--spec/lib/quality/seeders/issues_spec.rb18
-rw-r--r--spec/lib/quality/test_level_spec.rb105
-rw-r--r--spec/lib/sentry/client_spec.rb129
-rw-r--r--spec/mailers/abuse_report_mailer_spec.rb25
-rw-r--r--spec/mailers/email_rejection_mailer_spec.rb16
-rw-r--r--spec/mailers/emails/auto_devops_spec.rb3
-rw-r--r--spec/mailers/emails/issues_spec.rb9
-rw-r--r--spec/mailers/emails/pages_domains_spec.rb30
-rw-r--r--spec/mailers/notify_spec.rb207
-rw-r--r--spec/mailers/repository_check_mailer_spec.rb7
-rw-r--r--spec/migrations/add_foreign_keys_to_todos_spec.rb2
-rw-r--r--spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb24
-rw-r--r--spec/migrations/calculate_conv_dev_index_percentages_spec.rb22
-rw-r--r--spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb35
-rw-r--r--spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb34
-rw-r--r--spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb19
-rw-r--r--spec/migrations/delete_inconsistent_internal_id_records_spec.rb83
-rw-r--r--spec/migrations/enqueue_reset_merge_status_spec.rb49
-rw-r--r--spec/migrations/enqueue_verify_pages_domain_workers_spec.rb6
-rw-r--r--spec/migrations/generate_lets_encrypt_private_key_spec.rb12
-rw-r--r--spec/migrations/generate_missing_routes_spec.rb2
-rw-r--r--spec/migrations/issues_moved_to_id_foreign_key_spec.rb15
-rw-r--r--spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb6
-rw-r--r--spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb26
-rw-r--r--spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb24
-rw-r--r--spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb12
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb32
-rw-r--r--spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb20
-rw-r--r--spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb12
-rw-r--r--spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb22
-rw-r--r--spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb6
-rw-r--r--spec/migrations/migrate_user_project_view_spec.rb6
-rw-r--r--spec/migrations/move_personal_snippets_files_spec.rb35
-rw-r--r--spec/migrations/remove_orphaned_label_links_spec.rb6
-rw-r--r--spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb46
-rw-r--r--spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb47
-rw-r--r--spec/migrations/schedule_runners_token_encryption_spec.rb2
-rw-r--r--spec/migrations/schedule_sync_issuables_state_id_spec.rb81
-rw-r--r--spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb57
-rw-r--r--spec/migrations/truncate_user_fullname_spec.rb21
-rw-r--r--spec/models/ability_spec.rb2
-rw-r--r--spec/models/abuse_report_spec.rb2
-rw-r--r--spec/models/active_session_spec.rb53
-rw-r--r--spec/models/appearance_spec.rb20
-rw-r--r--spec/models/application_record_spec.rb25
-rw-r--r--spec/models/application_setting/term_spec.rb2
-rw-r--r--spec/models/application_setting_spec.rb305
-rw-r--r--spec/models/award_emoji_spec.rb2
-rw-r--r--spec/models/badge_spec.rb4
-rw-r--r--spec/models/badges/group_badge_spec.rb2
-rw-r--r--spec/models/badges/project_badge_spec.rb4
-rw-r--r--spec/models/blob_spec.rb17
-rw-r--r--spec/models/blob_viewer/base_spec.rb2
-rw-r--r--spec/models/blob_viewer/changelog_spec.rb2
-rw-r--r--spec/models/blob_viewer/composer_json_spec.rb2
-rw-r--r--spec/models/blob_viewer/gemspec_spec.rb2
-rw-r--r--spec/models/blob_viewer/gitlab_ci_yml_spec.rb2
-rw-r--r--spec/models/blob_viewer/license_spec.rb2
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb2
-rw-r--r--spec/models/blob_viewer/podspec_json_spec.rb2
-rw-r--r--spec/models/blob_viewer/podspec_spec.rb2
-rw-r--r--spec/models/blob_viewer/readme_spec.rb2
-rw-r--r--spec/models/blob_viewer/route_map_spec.rb2
-rw-r--r--spec/models/blob_viewer/server_side_spec.rb2
-rw-r--r--spec/models/board_group_recent_visit_spec.rb22
-rw-r--r--spec/models/board_project_recent_visit_spec.rb22
-rw-r--r--spec/models/board_spec.rb2
-rw-r--r--spec/models/broadcast_message_spec.rb8
-rw-r--r--spec/models/chat_name_spec.rb2
-rw-r--r--spec/models/chat_team_spec.rb2
-rw-r--r--spec/models/ci/artifact_blob_spec.rb2
-rw-r--r--spec/models/ci/bridge_spec.rb19
-rw-r--r--spec/models/ci/build_metadata_spec.rb2
-rw-r--r--spec/models/ci/build_runner_session_spec.rb18
-rw-r--r--spec/models/ci/build_spec.rb655
-rw-r--r--spec/models/ci/build_trace_chunk_spec.rb4
-rw-r--r--spec/models/ci/build_trace_chunks/database_spec.rb2
-rw-r--r--spec/models/ci/build_trace_chunks/fog_spec.rb2
-rw-r--r--spec/models/ci/build_trace_chunks/redis_spec.rb2
-rw-r--r--spec/models/ci/build_trace_section_name_spec.rb2
-rw-r--r--spec/models/ci/build_trace_section_spec.rb2
-rw-r--r--spec/models/ci/group_spec.rb2
-rw-r--r--spec/models/ci/group_variable_spec.rb6
-rw-r--r--spec/models/ci/job_artifact_spec.rb61
-rw-r--r--spec/models/ci/legacy_stage_spec.rb4
-rw-r--r--spec/models/ci/pipeline_schedule_spec.rb158
-rw-r--r--spec/models/ci/pipeline_schedule_variable_spec.rb4
-rw-r--r--spec/models/ci/pipeline_spec.rb700
-rw-r--r--spec/models/ci/pipeline_variable_spec.rb5
-rw-r--r--spec/models/ci/runner_spec.rb9
-rw-r--r--spec/models/ci/stage_spec.rb4
-rw-r--r--spec/models/ci/trigger_request_spec.rb2
-rw-r--r--spec/models/ci/trigger_spec.rb2
-rw-r--r--spec/models/ci/variable_spec.rb6
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb43
-rw-r--r--spec/models/clusters/applications/helm_spec.rb14
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb22
-rw-r--r--spec/models/clusters/applications/jupyter_spec.rb31
-rw-r--r--spec/models/clusters/applications/knative_spec.rb124
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb71
-rw-r--r--spec/models/clusters/applications/runner_spec.rb79
-rw-r--r--spec/models/clusters/cluster_spec.rb282
-rw-r--r--spec/models/clusters/kubernetes_namespace_spec.rb8
-rw-r--r--spec/models/clusters/platforms/kubernetes_spec.rb79
-rw-r--r--spec/models/clusters/project_spec.rb3
-rw-r--r--spec/models/clusters/providers/gcp_spec.rb2
-rw-r--r--spec/models/commit_collection_spec.rb102
-rw-r--r--spec/models/commit_range_spec.rb2
-rw-r--r--spec/models/commit_spec.rb18
-rw-r--r--spec/models/commit_status_spec.rb29
-rw-r--r--spec/models/compare_spec.rb2
-rw-r--r--spec/models/concerns/access_requestable_spec.rb2
-rw-r--r--spec/models/concerns/avatarable_spec.rb2
-rw-r--r--spec/models/concerns/awardable_spec.rb2
-rw-r--r--spec/models/concerns/batch_destroy_dependent_associations_spec.rb2
-rw-r--r--spec/models/concerns/blocks_json_serialization_spec.rb2
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb442
-rw-r--r--spec/models/concerns/cacheable_attributes_spec.rb2
-rw-r--r--spec/models/concerns/case_sensitivity_spec.rb2
-rw-r--r--spec/models/concerns/chronic_duration_attribute_spec.rb2
-rw-r--r--spec/models/concerns/deployable_spec.rb2
-rw-r--r--spec/models/concerns/deployment_platform_spec.rb2
-rw-r--r--spec/models/concerns/deprecated_assignee_spec.rb160
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb2
-rw-r--r--spec/models/concerns/each_batch_spec.rb2
-rw-r--r--spec/models/concerns/editable_spec.rb2
-rw-r--r--spec/models/concerns/expirable_spec.rb2
-rw-r--r--spec/models/concerns/faster_cache_keys_spec.rb2
-rw-r--r--spec/models/concerns/feature_gate_spec.rb2
-rw-r--r--spec/models/concerns/group_descendant_spec.rb2
-rw-r--r--spec/models/concerns/has_ref_spec.rb20
-rw-r--r--spec/models/concerns/has_status_spec.rb24
-rw-r--r--spec/models/concerns/has_variable_spec.rb4
-rw-r--r--spec/models/concerns/ignorable_column_spec.rb2
-rw-r--r--spec/models/concerns/issuable_spec.rb110
-rw-r--r--spec/models/concerns/issuable_states_spec.rb30
-rw-r--r--spec/models/concerns/loaded_in_group_list_spec.rb2
-rw-r--r--spec/models/concerns/manual_inverse_association_spec.rb2
-rw-r--r--spec/models/concerns/maskable_spec.rb76
-rw-r--r--spec/models/concerns/mentionable_spec.rb2
-rw-r--r--spec/models/concerns/milestoneish_spec.rb206
-rw-r--r--spec/models/concerns/noteable_spec.rb14
-rw-r--r--spec/models/concerns/participable_spec.rb2
-rw-r--r--spec/models/concerns/presentable_spec.rb2
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb2
-rw-r--r--spec/models/concerns/prometheus_adapter_spec.rb55
-rw-r--r--spec/models/concerns/protected_ref_access_spec.rb14
-rw-r--r--spec/models/concerns/reactive_caching_spec.rb46
-rw-r--r--spec/models/concerns/redactable_spec.rb2
-rw-r--r--spec/models/concerns/redis_cacheable_spec.rb2
-rw-r--r--spec/models/concerns/relative_positioning_spec.rb2
-rw-r--r--spec/models/concerns/resolvable_discussion_spec.rb2
-rw-r--r--spec/models/concerns/resolvable_note_spec.rb2
-rw-r--r--spec/models/concerns/routable_spec.rb2
-rw-r--r--spec/models/concerns/sha_attribute_spec.rb2
-rw-r--r--spec/models/concerns/sortable_spec.rb4
-rw-r--r--spec/models/concerns/spammable_spec.rb6
-rw-r--r--spec/models/concerns/strip_attribute_spec.rb2
-rw-r--r--spec/models/concerns/subscribable_spec.rb2
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb2
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/base_spec.rb34
-rw-r--r--spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb30
-rw-r--r--spec/models/concerns/triggerable_hooks_spec.rb2
-rw-r--r--spec/models/concerns/uniquify_spec.rb2
-rw-r--r--spec/models/container_repository_spec.rb2
-rw-r--r--spec/models/conversational_development_index/metric_spec.rb2
-rw-r--r--spec/models/cycle_analytics/code_spec.rb2
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb2
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb2
-rw-r--r--spec/models/cycle_analytics/production_spec.rb2
-rw-r--r--spec/models/cycle_analytics/review_spec.rb2
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb2
-rw-r--r--spec/models/cycle_analytics/test_spec.rb2
-rw-r--r--spec/models/cycle_analytics_spec.rb2
-rw-r--r--spec/models/deploy_key_spec.rb2
-rw-r--r--spec/models/deploy_keys_project_spec.rb2
-rw-r--r--spec/models/deploy_token_spec.rb34
-rw-r--r--spec/models/deployment_spec.rb61
-rw-r--r--spec/models/diff_discussion_spec.rb2
-rw-r--r--spec/models/diff_note_spec.rb20
-rw-r--r--spec/models/diff_viewer/base_spec.rb2
-rw-r--r--spec/models/diff_viewer/server_side_spec.rb2
-rw-r--r--spec/models/discussion_spec.rb2
-rw-r--r--spec/models/email_spec.rb2
-rw-r--r--spec/models/environment_spec.rb51
-rw-r--r--spec/models/environment_status_spec.rb6
-rw-r--r--spec/models/error_tracking/project_error_tracking_setting_spec.rb63
-rw-r--r--spec/models/event_collection_spec.rb2
-rw-r--r--spec/models/event_spec.rb6
-rw-r--r--spec/models/external_issue_spec.rb2
-rw-r--r--spec/models/fork_network_member_spec.rb2
-rw-r--r--spec/models/fork_network_spec.rb2
-rw-r--r--spec/models/generic_commit_status_spec.rb2
-rw-r--r--spec/models/global_milestone_spec.rb2
-rw-r--r--spec/models/gpg_key_spec.rb2
-rw-r--r--spec/models/gpg_key_subkey_spec.rb2
-rw-r--r--spec/models/gpg_signature_spec.rb2
-rw-r--r--spec/models/group_custom_attribute_spec.rb2
-rw-r--r--spec/models/group_label_spec.rb2
-rw-r--r--spec/models/group_milestone_spec.rb2
-rw-r--r--spec/models/group_spec.rb161
-rw-r--r--spec/models/guest_spec.rb2
-rw-r--r--spec/models/hooks/active_hook_filter_spec.rb2
-rw-r--r--spec/models/hooks/project_hook_spec.rb2
-rw-r--r--spec/models/hooks/service_hook_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb2
-rw-r--r--spec/models/hooks/web_hook_log_spec.rb2
-rw-r--r--spec/models/hooks/web_hook_spec.rb2
-rw-r--r--spec/models/identity_spec.rb2
-rw-r--r--spec/models/import_export_upload_spec.rb2
-rw-r--r--spec/models/instance_configuration_spec.rb13
-rw-r--r--spec/models/internal_id_spec.rb64
-rw-r--r--spec/models/issue/metrics_spec.rb8
-rw-r--r--spec/models/issue_collection_spec.rb2
-rw-r--r--spec/models/issue_spec.rb85
-rw-r--r--spec/models/key_spec.rb2
-rw-r--r--spec/models/label_link_spec.rb2
-rw-r--r--spec/models/label_priority_spec.rb2
-rw-r--r--spec/models/label_spec.rb2
-rw-r--r--spec/models/legacy_diff_discussion_spec.rb2
-rw-r--r--spec/models/lfs_download_object_spec.rb2
-rw-r--r--spec/models/lfs_file_lock_spec.rb2
-rw-r--r--spec/models/lfs_object_spec.rb2
-rw-r--r--spec/models/lfs_objects_project_spec.rb2
-rw-r--r--spec/models/license_template_spec.rb15
-rw-r--r--spec/models/list_spec.rb2
-rw-r--r--spec/models/member_spec.rb12
-rw-r--r--spec/models/members/group_member_spec.rb18
-rw-r--r--spec/models/members/project_member_spec.rb2
-rw-r--r--spec/models/merge_request/metrics_spec.rb2
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb2
-rw-r--r--spec/models/merge_request_diff_file_spec.rb2
-rw-r--r--spec/models/merge_request_diff_spec.rb184
-rw-r--r--spec/models/merge_request_spec.rb427
-rw-r--r--spec/models/milestone_spec.rb64
-rw-r--r--spec/models/namespace_spec.rb98
-rw-r--r--spec/models/network/graph_spec.rb4
-rw-r--r--spec/models/note_diff_file_spec.rb29
-rw-r--r--spec/models/note_spec.rb20
-rw-r--r--spec/models/notification_recipient_spec.rb42
-rw-r--r--spec/models/notification_setting_spec.rb2
-rw-r--r--spec/models/pages_domain_acme_order_spec.rb49
-rw-r--r--spec/models/pages_domain_spec.rb41
-rw-r--r--spec/models/personal_access_token_spec.rb2
-rw-r--r--spec/models/pool_repository_spec.rb8
-rw-r--r--spec/models/programming_language_spec.rb2
-rw-r--r--spec/models/project_authorization_spec.rb2
-rw-r--r--spec/models/project_auto_devops_spec.rb78
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb28
-rw-r--r--spec/models/project_custom_attribute_spec.rb2
-rw-r--r--spec/models/project_daily_statistic_spec.rb7
-rw-r--r--spec/models/project_deploy_token_spec.rb2
-rw-r--r--spec/models/project_feature_spec.rb2
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_import_state_spec.rb2
-rw-r--r--spec/models/project_label_spec.rb2
-rw-r--r--spec/models/project_metrics_setting_spec.rb55
-rw-r--r--spec/models/project_services/asana_service_spec.rb2
-rw-r--r--spec/models/project_services/assembla_service_spec.rb8
-rw-r--r--spec/models/project_services/bamboo_service_spec.rb5
-rw-r--r--spec/models/project_services/bugzilla_service_spec.rb2
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb12
-rw-r--r--spec/models/project_services/campfire_service_spec.rb26
-rw-r--r--spec/models/project_services/chat_message/deployment_message_spec.rb153
-rw-r--r--spec/models/project_services/chat_message/issue_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/merge_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/note_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/pipeline_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/push_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_message/wiki_page_message_spec.rb2
-rw-r--r--spec/models/project_services/chat_notification_service_spec.rb2
-rw-r--r--spec/models/project_services/custom_issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb2
-rw-r--r--spec/models/project_services/emails_on_push_service_spec.rb2
-rw-r--r--spec/models/project_services/external_wiki_service_spec.rb2
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb2
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb409
-rw-r--r--spec/models/project_services/irker_service_spec.rb2
-rw-r--r--spec/models/project_services/issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb12
-rw-r--r--spec/models/project_services/kubernetes_service_spec.rb54
-rw-r--r--spec/models/project_services/mattermost_service_spec.rb2
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb2
-rw-r--r--spec/models/project_services/microsoft_teams_service_spec.rb8
-rw-r--r--spec/models/project_services/packagist_service_spec.rb2
-rw-r--r--spec/models/project_services/pipelines_email_service_spec.rb78
-rw-r--r--spec/models/project_services/pivotaltracker_service_spec.rb18
-rw-r--r--spec/models/project_services/prometheus_service_spec.rb61
-rw-r--r--spec/models/project_services/pushover_service_spec.rb8
-rw-r--r--spec/models/project_services/redmine_service_spec.rb2
-rw-r--r--spec/models/project_services/slack_service_spec.rb2
-rw-r--r--spec/models/project_services/slack_slash_commands_service_spec.rb2
-rw-r--r--spec/models/project_services/teamcity_service_spec.rb5
-rw-r--r--spec/models/project_services/youtrack_service_spec.rb40
-rw-r--r--spec/models/project_snippet_spec.rb2
-rw-r--r--spec/models/project_spec.rb507
-rw-r--r--spec/models/project_statistics_spec.rb114
-rw-r--r--spec/models/project_team_spec.rb26
-rw-r--r--spec/models/project_wiki_spec.rb85
-rw-r--r--spec/models/protectable_dropdown_spec.rb2
-rw-r--r--spec/models/protected_branch/merge_access_level_spec.rb2
-rw-r--r--spec/models/protected_branch/push_access_level_spec.rb2
-rw-r--r--spec/models/protected_branch_spec.rb30
-rw-r--r--spec/models/protected_tag_spec.rb2
-rw-r--r--spec/models/push_event_payload_spec.rb2
-rw-r--r--spec/models/push_event_spec.rb6
-rw-r--r--spec/models/redirect_route_spec.rb2
-rw-r--r--spec/models/release_spec.rb23
-rw-r--r--spec/models/remote_mirror_spec.rb22
-rw-r--r--spec/models/repository_language_spec.rb2
-rw-r--r--spec/models/repository_spec.rb330
-rw-r--r--spec/models/resource_label_event_spec.rb4
-rw-r--r--spec/models/route_spec.rb2
-rw-r--r--spec/models/sent_notification_spec.rb2
-rw-r--r--spec/models/serverless/function_spec.rb21
-rw-r--r--spec/models/service_spec.rb6
-rw-r--r--spec/models/snippet_blob_spec.rb2
-rw-r--r--spec/models/snippet_spec.rb2
-rw-r--r--spec/models/spam_log_spec.rb2
-rw-r--r--spec/models/ssh_host_key_spec.rb2
-rw-r--r--spec/models/subscription_spec.rb2
-rw-r--r--spec/models/suggestion_spec.rb16
-rw-r--r--spec/models/system_note_metadata_spec.rb2
-rw-r--r--spec/models/term_agreement_spec.rb2
-rw-r--r--spec/models/timelog_spec.rb2
-rw-r--r--spec/models/todo_spec.rb2
-rw-r--r--spec/models/tree_spec.rb2
-rw-r--r--spec/models/trending_project_spec.rb2
-rw-r--r--spec/models/upload_spec.rb2
-rw-r--r--spec/models/user_agent_detail_spec.rb2
-rw-r--r--spec/models/user_callout_spec.rb2
-rw-r--r--spec/models/user_custom_attribute_spec.rb2
-rw-r--r--spec/models/user_interacted_project_spec.rb2
-rw-r--r--spec/models/user_preference_spec.rb6
-rw-r--r--spec/models/user_spec.rb75
-rw-r--r--spec/models/wiki_directory_spec.rb2
-rw-r--r--spec/models/wiki_page_spec.rb69
-rw-r--r--spec/policies/base_policy_spec.rb23
-rw-r--r--spec/policies/board_policy_spec.rb8
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb46
-rw-r--r--spec/policies/clusters/cluster_policy_spec.rb16
-rw-r--r--spec/policies/clusters/instance_policy_spec.rb36
-rw-r--r--spec/policies/commit_policy_spec.rb55
-rw-r--r--spec/policies/global_policy_spec.rb12
-rw-r--r--spec/policies/group_member_policy_spec.rb105
-rw-r--r--spec/policies/group_policy_spec.rb203
-rw-r--r--spec/policies/identity_provider_policy_spec.rb30
-rw-r--r--spec/policies/issuable_policy_spec.rb6
-rw-r--r--spec/policies/issue_policy_spec.rb19
-rw-r--r--spec/policies/merge_request_policy_spec.rb69
-rw-r--r--spec/policies/namespace_policy_spec.rb2
-rw-r--r--spec/policies/note_policy_spec.rb94
-rw-r--r--spec/policies/personal_snippet_policy_spec.rb31
-rw-r--r--spec/policies/project_policy_spec.rb309
-rw-r--r--spec/policies/project_snippet_policy_spec.rb109
-rw-r--r--spec/presenters/blob_presenter_spec.rb10
-rw-r--r--spec/presenters/blobs/unfold_presenter_spec.rb159
-rw-r--r--spec/presenters/ci/bridge_presenter_spec.rb15
-rw-r--r--spec/presenters/ci/build_presenter_spec.rb12
-rw-r--r--spec/presenters/ci/build_runner_presenter_spec.rb64
-rw-r--r--spec/presenters/ci/pipeline_presenter_spec.rb134
-rw-r--r--spec/presenters/clusters/cluster_presenter_spec.rb68
-rw-r--r--spec/presenters/group_clusterable_presenter_spec.rb8
-rw-r--r--spec/presenters/issue_presenter_spec.rb29
-rw-r--r--spec/presenters/label_presenter_spec.rb93
-rw-r--r--spec/presenters/merge_request_presenter_spec.rb155
-rw-r--r--spec/presenters/project_clusterable_presenter_spec.rb8
-rw-r--r--spec/presenters/project_presenter_spec.rb19
-rw-r--r--spec/presenters/tree_entry_presenter_spec.rb16
-rw-r--r--spec/rack_servers/configs/puma.rb32
-rw-r--r--spec/rack_servers/puma_spec.rb28
-rw-r--r--spec/requests/api/badges_spec.rb6
-rw-r--r--spec/requests/api/branches_spec.rb26
-rw-r--r--spec/requests/api/circuit_breakers_spec.rb46
-rw-r--r--spec/requests/api/commit_statuses_spec.rb17
-rw-r--r--spec/requests/api/commits_spec.rb107
-rw-r--r--spec/requests/api/discussions_spec.rb4
-rw-r--r--spec/requests/api/environments_spec.rb22
-rw-r--r--spec/requests/api/events_spec.rb135
-rw-r--r--spec/requests/api/graphql/gitlab_schema_spec.rb150
-rw-r--r--spec/requests/api/graphql/group_query_spec.rb119
-rw-r--r--spec/requests/api/graphql/metadata_query_spec.rb32
-rw-r--r--spec/requests/api/graphql/multiplexed_queries_spec.rb39
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb2
-rw-r--r--spec/requests/api/graphql/namespace/projects_spec.rb82
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb28
-rw-r--r--spec/requests/api/graphql/project/merge_request_spec.rb4
-rw-r--r--spec/requests/api/graphql/project/project_statistics_spec.rb43
-rw-r--r--spec/requests/api/graphql/project/repository_spec.rb37
-rw-r--r--spec/requests/api/graphql/project/tree/tree_spec.rb73
-rw-r--r--spec/requests/api/graphql_spec.rb134
-rw-r--r--spec/requests/api/group_variables_spec.rb12
-rw-r--r--spec/requests/api/groups_spec.rb3
-rw-r--r--spec/requests/api/helpers_spec.rb7
-rw-r--r--spec/requests/api/internal_spec.rb172
-rw-r--r--spec/requests/api/issuable_bulk_update_spec.rb154
-rw-r--r--spec/requests/api/issues/get_group_issues_spec.rb652
-rw-r--r--spec/requests/api/issues/get_project_issues_spec.rb807
-rw-r--r--spec/requests/api/issues/issues_spec.rb796
-rw-r--r--spec/requests/api/issues/post_projects_issues_spec.rb549
-rw-r--r--spec/requests/api/issues/put_projects_issues_spec.rb392
-rw-r--r--spec/requests/api/issues_spec.rb2041
-rw-r--r--spec/requests/api/jobs_spec.rb48
-rw-r--r--spec/requests/api/keys_spec.rb2
-rw-r--r--spec/requests/api/members_spec.rb15
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb4
-rw-r--r--spec/requests/api/merge_requests_spec.rb892
-rw-r--r--spec/requests/api/namespaces_spec.rb2
-rw-r--r--spec/requests/api/pipeline_schedules_spec.rb7
-rw-r--r--spec/requests/api/pipelines_spec.rb88
-rw-r--r--spec/requests/api/project_clusters_spec.rb75
-rw-r--r--spec/requests/api/project_events_spec.rb156
-rw-r--r--spec/requests/api/project_milestones_spec.rb74
-rw-r--r--spec/requests/api/project_statistics_spec.rb62
-rw-r--r--spec/requests/api/projects_spec.rb240
-rw-r--r--spec/requests/api/release/links_spec.rb16
-rw-r--r--spec/requests/api/releases_spec.rb83
-rw-r--r--spec/requests/api/runner_spec.rb165
-rw-r--r--spec/requests/api/runners_spec.rb45
-rw-r--r--spec/requests/api/search_spec.rb136
-rw-r--r--spec/requests/api/settings_spec.rb38
-rw-r--r--spec/requests/api/snippets_spec.rb76
-rw-r--r--spec/requests/api/suggestions_spec.rb3
-rw-r--r--spec/requests/api/system_hooks_spec.rb8
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/api/task_completion_status_spec.rb85
-rw-r--r--spec/requests/api/todos_spec.rb52
-rw-r--r--spec/requests/api/users_spec.rb60
-rw-r--r--spec/requests/api/variables_spec.rb15
-rw-r--r--spec/requests/api/version_spec.rb18
-rw-r--r--spec/requests/git_http_spec.rb185
-rw-r--r--spec/requests/jwt_controller_spec.rb2
-rw-r--r--spec/requests/openid_connect_spec.rb2
-rw-r--r--spec/requests/rack_attack_global_spec.rb26
-rw-r--r--spec/routing/api_routing_spec.rb14
-rw-r--r--spec/routing/group_routing_spec.rb18
-rw-r--r--spec/routing/import_routing_spec.rb12
-rw-r--r--spec/routing/project_routing_spec.rb84
-rw-r--r--spec/routing/uploads_routing_spec.rb22
-rw-r--r--spec/rubocop/cop/active_record_association_reload_spec.rb60
-rw-r--r--spec/rubocop/cop/code_reuse/active_record_spec.rb6
-rw-r--r--spec/rubocop/cop/include_action_view_context_spec.rb45
-rw-r--r--spec/rubocop/cop/migration/update_column_in_batches_spec.rb18
-rw-r--r--spec/rubocop/cop/qa/element_with_pattern_spec.rb11
-rw-r--r--spec/serializers/analytics_stage_serializer_spec.rb32
-rw-r--r--spec/serializers/analytics_summary_serializer_spec.rb2
-rw-r--r--spec/serializers/build_details_entity_spec.rb43
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb4
-rw-r--r--spec/serializers/deployment_entity_spec.rb41
-rw-r--r--spec/serializers/environment_entity_spec.rb4
-rw-r--r--spec/serializers/group_child_entity_spec.rb19
-rw-r--r--spec/serializers/job_artifact_report_entity_spec.rb28
-rw-r--r--spec/serializers/job_entity_spec.rb18
-rw-r--r--spec/serializers/merge_request_for_pipeline_entity_spec.rb29
-rw-r--r--spec/serializers/merge_request_widget_entity_spec.rb65
-rw-r--r--spec/serializers/pipeline_entity_spec.rb88
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb46
-rw-r--r--spec/serializers/provider_repo_entity_spec.rb2
-rw-r--r--spec/serializers/stage_entity_spec.rb28
-rw-r--r--spec/serializers/stage_serializer_spec.rb31
-rw-r--r--spec/serializers/suggestion_entity_spec.rb3
-rw-r--r--spec/serializers/test_case_entity_spec.rb2
-rw-r--r--spec/services/access_token_validation_service_spec.rb2
-rw-r--r--spec/services/after_branch_delete_service_spec.rb15
-rw-r--r--spec/services/application_settings/update_service_spec.rb37
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb95
-rw-r--r--spec/services/auto_merge/base_service_spec.rb144
-rw-r--r--spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb (renamed from spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb)65
-rw-r--r--spec/services/auto_merge_service_spec.rb140
-rw-r--r--spec/services/base_count_service_spec.rb2
-rw-r--r--spec/services/boards/create_service_spec.rb2
-rw-r--r--spec/services/boards/issues/create_service_spec.rb2
-rw-r--r--spec/services/boards/issues/list_service_spec.rb2
-rw-r--r--spec/services/boards/issues/move_service_spec.rb2
-rw-r--r--spec/services/boards/list_service_spec.rb2
-rw-r--r--spec/services/boards/lists/create_service_spec.rb2
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb2
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb2
-rw-r--r--spec/services/boards/lists/list_service_spec.rb2
-rw-r--r--spec/services/boards/lists/move_service_spec.rb2
-rw-r--r--spec/services/boards/visits/latest_service_spec.rb12
-rw-r--r--spec/services/chat_names/authorize_user_service_spec.rb2
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb2
-rw-r--r--spec/services/ci/archive_trace_service_spec.rb2
-rw-r--r--spec/services/ci/compare_test_reports_service_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb174
-rw-r--r--spec/services/ci/destroy_expired_job_artifacts_service_spec.rb2
-rw-r--r--spec/services/ci/destroy_pipeline_service_spec.rb15
-rw-r--r--spec/services/ci/ensure_stage_service_spec.rb2
-rw-r--r--spec/services/ci/expire_pipeline_cache_service_spec.rb61
-rw-r--r--spec/services/ci/extract_sections_from_build_trace_service_spec.rb2
-rw-r--r--spec/services/ci/pipeline_schedule_service_spec.rb28
-rw-r--r--spec/services/ci/pipeline_trigger_service_spec.rb2
-rw-r--r--spec/services/ci/play_build_service_spec.rb2
-rw-r--r--spec/services/ci/play_manual_stage_service_spec.rb79
-rw-r--r--spec/services/ci/prepare_build_service_spec.rb68
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/register_job_service_spec.rb2
-rw-r--r--spec/services/ci/retry_build_service_spec.rb17
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/run_scheduled_build_service_spec.rb2
-rw-r--r--spec/services/ci/stop_environments_service_spec.rb78
-rw-r--r--spec/services/ci/update_build_queue_service_spec.rb2
-rw-r--r--spec/services/ci/update_runner_service_spec.rb2
-rw-r--r--spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb12
-rw-r--r--spec/services/clusters/applications/check_installation_progress_service_spec.rb30
-rw-r--r--spec/services/clusters/applications/check_uninstall_progress_service_spec.rb145
-rw-r--r--spec/services/clusters/applications/create_service_spec.rb222
-rw-r--r--spec/services/clusters/applications/destroy_service_spec.rb63
-rw-r--r--spec/services/clusters/applications/install_service_spec.rb80
-rw-r--r--spec/services/clusters/applications/patch_service_spec.rb80
-rw-r--r--spec/services/clusters/applications/schedule_installation_service_spec.rb77
-rw-r--r--spec/services/clusters/applications/uninstall_service_spec.rb77
-rw-r--r--spec/services/clusters/applications/update_service_spec.rb72
-rw-r--r--spec/services/clusters/applications/upgrade_service_spec.rb76
-rw-r--r--spec/services/clusters/build_service_spec.rb8
-rw-r--r--spec/services/clusters/create_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/fetch_operation_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/provision_service_spec.rb2
-rw-r--r--spec/services/clusters/gcp/verify_provision_status_service_spec.rb2
-rw-r--r--spec/services/clusters/refresh_service_spec.rb8
-rw-r--r--spec/services/clusters/update_service_spec.rb2
-rw-r--r--spec/services/cohorts_service_spec.rb2
-rw-r--r--spec/services/compare_service_spec.rb15
-rw-r--r--spec/services/create_branch_service_spec.rb2
-rw-r--r--spec/services/create_snippet_service_spec.rb2
-rw-r--r--spec/services/delete_branch_service_spec.rb26
-rw-r--r--spec/services/delete_merged_branches_service_spec.rb2
-rw-r--r--spec/services/deploy_keys/create_service_spec.rb2
-rw-r--r--spec/services/deploy_tokens/create_service_spec.rb12
-rw-r--r--spec/services/discussions/resolve_service_spec.rb2
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb2
-rw-r--r--spec/services/emails/confirm_service_spec.rb2
-rw-r--r--spec/services/emails/create_service_spec.rb2
-rw-r--r--spec/services/emails/destroy_service_spec.rb2
-rw-r--r--spec/services/error_tracking/list_issues_service_spec.rb24
-rw-r--r--spec/services/error_tracking/list_projects_service_spec.rb30
-rw-r--r--spec/services/event_create_service_spec.rb2
-rw-r--r--spec/services/events/render_service_spec.rb2
-rw-r--r--spec/services/files/create_service_spec.rb2
-rw-r--r--spec/services/files/delete_service_spec.rb2
-rw-r--r--spec/services/files/multi_service_spec.rb18
-rw-r--r--spec/services/files/update_service_spec.rb2
-rw-r--r--spec/services/git/base_hooks_service_spec.rb90
-rw-r--r--spec/services/git/branch_hooks_service_spec.rb347
-rw-r--r--spec/services/git/branch_push_service_spec.rb (renamed from spec/services/git_push_service_spec.rb)328
-rw-r--r--spec/services/git/tag_hooks_service_spec.rb152
-rw-r--r--spec/services/git/tag_push_service_spec.rb61
-rw-r--r--spec/services/git_tag_push_service_spec.rb196
-rw-r--r--spec/services/gpg_keys/create_service_spec.rb2
-rw-r--r--spec/services/gravatar_service_spec.rb2
-rw-r--r--spec/services/groups/auto_devops_service_spec.rb62
-rw-r--r--spec/services/groups/create_service_spec.rb13
-rw-r--r--spec/services/groups/destroy_service_spec.rb40
-rw-r--r--spec/services/groups/nested_create_service_spec.rb2
-rw-r--r--spec/services/groups/transfer_service_spec.rb113
-rw-r--r--spec/services/groups/update_service_spec.rb2
-rw-r--r--spec/services/import_export_clean_up_service_spec.rb2
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb18
-rw-r--r--spec/services/issuable/clone/content_rewriter_spec.rb16
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb6
-rw-r--r--spec/services/issuable/destroy_service_spec.rb4
-rw-r--r--spec/services/issues/build_service_spec.rb82
-rw-r--r--spec/services/issues/close_service_spec.rb46
-rw-r--r--spec/services/issues/create_service_spec.rb16
-rw-r--r--spec/services/issues/duplicate_service_spec.rb2
-rw-r--r--spec/services/issues/move_service_spec.rb2
-rw-r--r--spec/services/issues/related_branches_service_spec.rb2
-rw-r--r--spec/services/issues/reopen_service_spec.rb2
-rw-r--r--spec/services/issues/resolve_discussions_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb18
-rw-r--r--spec/services/keys/create_service_spec.rb2
-rw-r--r--spec/services/keys/destroy_service_spec.rb2
-rw-r--r--spec/services/keys/last_used_service_spec.rb2
-rw-r--r--spec/services/labels/available_labels_service_spec.rb86
-rw-r--r--spec/services/labels/create_service_spec.rb2
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb2
-rw-r--r--spec/services/labels/promote_service_spec.rb2
-rw-r--r--spec/services/labels/transfer_service_spec.rb2
-rw-r--r--spec/services/labels/update_service_spec.rb2
-rw-r--r--spec/services/lfs/file_transformer_spec.rb21
-rw-r--r--spec/services/lfs/lock_file_service_spec.rb2
-rw-r--r--spec/services/lfs/locks_finder_service_spec.rb2
-rw-r--r--spec/services/lfs/unlock_file_service_spec.rb2
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb2
-rw-r--r--spec/services/members/create_service_spec.rb15
-rw-r--r--spec/services/members/destroy_service_spec.rb6
-rw-r--r--spec/services/members/request_access_service_spec.rb2
-rw-r--r--spec/services/members/update_service_spec.rb2
-rw-r--r--spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb34
-rw-r--r--spec/services/merge_requests/assign_issues_service_spec.rb2
-rw-r--r--spec/services/merge_requests/build_service_spec.rb11
-rw-r--r--spec/services/merge_requests/close_service_spec.rb20
-rw-r--r--spec/services/merge_requests/conflicts/list_service_spec.rb2
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb2
-rw-r--r--spec/services/merge_requests/create_from_issue_service_spec.rb4
-rw-r--r--spec/services/merge_requests/create_pipeline_service_spec.rb71
-rw-r--r--spec/services/merge_requests/create_service_spec.rb99
-rw-r--r--spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb2
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb6
-rw-r--r--spec/services/merge_requests/get_urls_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb20
-rw-r--r--spec/services/merge_requests/merge_to_ref_service_spec.rb174
-rw-r--r--spec/services/merge_requests/mergeability_check_service_spec.rb187
-rw-r--r--spec/services/merge_requests/migrate_external_diffs_service_spec.rb43
-rw-r--r--spec/services/merge_requests/post_merge_service_spec.rb12
-rw-r--r--spec/services/merge_requests/push_options_handler_service_spec.rb405
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb46
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb154
-rw-r--r--spec/services/merge_requests/reload_diffs_service_spec.rb2
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb4
-rw-r--r--spec/services/merge_requests/resolved_discussion_notification_service_spec.rb2
-rw-r--r--spec/services/merge_requests/squash_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb55
-rw-r--r--spec/services/milestones/close_service_spec.rb2
-rw-r--r--spec/services/milestones/create_service_spec.rb2
-rw-r--r--spec/services/milestones/destroy_service_spec.rb2
-rw-r--r--spec/services/milestones/promote_service_spec.rb2
-rw-r--r--spec/services/note_summary_spec.rb12
-rw-r--r--spec/services/notes/build_service_spec.rb38
-rw-r--r--spec/services/notes/create_service_spec.rb56
-rw-r--r--spec/services/notes/destroy_service_spec.rb2
-rw-r--r--spec/services/notes/post_process_service_spec.rb2
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb27
-rw-r--r--spec/services/notes/render_service_spec.rb2
-rw-r--r--spec/services/notes/resolve_service_spec.rb2
-rw-r--r--spec/services/notes/update_service_spec.rb2
-rw-r--r--spec/services/notification_recipient_service_spec.rb2
-rw-r--r--spec/services/notification_service_spec.rb162
-rw-r--r--spec/services/pages_domains/create_acme_order_service_spec.rb63
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb146
-rw-r--r--spec/services/preview_markdown_service_spec.rb77
-rw-r--r--spec/services/projects/after_import_service_spec.rb2
-rw-r--r--spec/services/projects/auto_devops/disable_service_spec.rb10
-rw-r--r--spec/services/projects/autocomplete_service_spec.rb2
-rw-r--r--spec/services/projects/batch_open_issues_count_service_spec.rb2
-rw-r--r--spec/services/projects/cleanup_service_spec.rb91
-rw-r--r--spec/services/projects/count_service_spec.rb2
-rw-r--r--spec/services/projects/create_from_template_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb65
-rw-r--r--spec/services/projects/destroy_service_spec.rb8
-rw-r--r--spec/services/projects/detect_repository_languages_service_spec.rb14
-rw-r--r--spec/services/projects/download_service_spec.rb2
-rw-r--r--spec/services/projects/enable_deploy_key_service_spec.rb2
-rw-r--r--spec/services/projects/fetch_statistics_increment_service_spec.rb36
-rw-r--r--spec/services/projects/fork_service_spec.rb26
-rw-r--r--spec/services/projects/forks_count_service_spec.rb2
-rw-r--r--spec/services/projects/git_deduplication_service_spec.rb90
-rw-r--r--spec/services/projects/gitlab_projects_import_service_spec.rb2
-rw-r--r--spec/services/projects/group_links/create_service_spec.rb10
-rw-r--r--spec/services/projects/group_links/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb14
-rw-r--r--spec/services/projects/hashed_storage/migrate_repository_service_spec.rb20
-rw-r--r--spec/services/projects/hashed_storage/migration_service_spec.rb2
-rw-r--r--spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb106
-rw-r--r--spec/services/projects/hashed_storage/rollback_repository_service_spec.rb117
-rw-r--r--spec/services/projects/hashed_storage/rollback_service_spec.rb57
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb18
-rw-r--r--spec/services/projects/import_export/export_service_spec.rb2
-rw-r--r--spec/services/projects/import_service_spec.rb47
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb23
-rw-r--r--spec/services/projects/lfs_pointers/lfs_download_service_spec.rb20
-rw-r--r--spec/services/projects/lfs_pointers/lfs_import_service_spec.rb153
-rw-r--r--spec/services/projects/lfs_pointers/lfs_link_service_spec.rb1
-rw-r--r--spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb148
-rw-r--r--spec/services/projects/move_access_service_spec.rb2
-rw-r--r--spec/services/projects/move_deploy_keys_projects_service_spec.rb2
-rw-r--r--spec/services/projects/move_forks_service_spec.rb2
-rw-r--r--spec/services/projects/move_lfs_objects_projects_service_spec.rb2
-rw-r--r--spec/services/projects/move_notification_settings_service_spec.rb2
-rw-r--r--spec/services/projects/move_project_authorizations_service_spec.rb2
-rw-r--r--spec/services/projects/move_project_group_links_service_spec.rb2
-rw-r--r--spec/services/projects/move_project_members_service_spec.rb2
-rw-r--r--spec/services/projects/move_users_star_projects_service_spec.rb2
-rw-r--r--spec/services/projects/open_issues_count_service_spec.rb2
-rw-r--r--spec/services/projects/open_merge_requests_count_service_spec.rb2
-rw-r--r--spec/services/projects/operations/update_service_spec.rb100
-rw-r--r--spec/services/projects/overwrite_project_service_spec.rb2
-rw-r--r--spec/services/projects/participants_service_spec.rb57
-rw-r--r--spec/services/projects/propagate_service_template_spec.rb2
-rw-r--r--spec/services/projects/repository_languages_service_spec.rb50
-rw-r--r--spec/services/projects/transfer_service_spec.rb28
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb2
-rw-r--r--spec/services/projects/update_pages_configuration_service_spec.rb2
-rw-r--r--spec/services/projects/update_pages_service_spec.rb61
-rw-r--r--spec/services/projects/update_remote_mirror_service_spec.rb2
-rw-r--r--spec/services/projects/update_service_spec.rb46
-rw-r--r--spec/services/projects/update_statistics_service_spec.rb32
-rw-r--r--spec/services/prometheus/adapter_service_spec.rb2
-rw-r--r--spec/services/prometheus/proxy_service_spec.rb195
-rw-r--r--spec/services/protected_branches/create_service_spec.rb2
-rw-r--r--spec/services/protected_branches/destroy_service_spec.rb2
-rw-r--r--spec/services/protected_branches/update_service_spec.rb2
-rw-r--r--spec/services/protected_tags/create_service_spec.rb2
-rw-r--r--spec/services/protected_tags/destroy_service_spec.rb2
-rw-r--r--spec/services/protected_tags/update_service_spec.rb2
-rw-r--r--spec/services/push_event_payload_service_spec.rb2
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb56
-rw-r--r--spec/services/releases/create_service_spec.rb12
-rw-r--r--spec/services/releases/destroy_service_spec.rb10
-rw-r--r--spec/services/releases/update_service_spec.rb2
-rw-r--r--spec/services/repair_ldap_blocked_user_service_spec.rb2
-rw-r--r--spec/services/repository_archive_clean_up_service_spec.rb2
-rw-r--r--spec/services/reset_project_cache_service_spec.rb2
-rw-r--r--spec/services/search/global_service_spec.rb2
-rw-r--r--spec/services/search/group_service_spec.rb2
-rw-r--r--spec/services/search/snippet_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb2
-rw-r--r--spec/services/service_response_spec.rb73
-rw-r--r--spec/services/spam_service_spec.rb2
-rw-r--r--spec/services/submit_usage_ping_service_spec.rb6
-rw-r--r--spec/services/suggestions/apply_service_spec.rb232
-rw-r--r--spec/services/suggestions/create_service_spec.rb110
-rw-r--r--spec/services/suggestions/outdate_service_spec.rb102
-rw-r--r--spec/services/system_hooks_service_spec.rb14
-rw-r--r--spec/services/system_note_service_spec.rb102
-rw-r--r--spec/services/tags/create_service_spec.rb4
-rw-r--r--spec/services/tags/destroy_service_spec.rb18
-rw-r--r--spec/services/task_list_toggle_service_spec.rb21
-rw-r--r--spec/services/test_hooks/project_service_spec.rb2
-rw-r--r--spec/services/test_hooks/system_service_spec.rb2
-rw-r--r--spec/services/todo_service_spec.rb112
-rw-r--r--spec/services/todos/destroy/confidential_issue_service_spec.rb60
-rw-r--r--spec/services/todos/destroy/entity_leave_service_spec.rb16
-rw-r--r--spec/services/todos/destroy/group_private_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/private_features_service_spec.rb2
-rw-r--r--spec/services/todos/destroy/project_private_service_spec.rb2
-rw-r--r--spec/services/update_deployment_service_spec.rb40
-rw-r--r--spec/services/update_merge_request_metrics_service_spec.rb2
-rw-r--r--spec/services/update_snippet_service_spec.rb2
-rw-r--r--spec/services/upload_service_spec.rb2
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb2
-rw-r--r--spec/services/users/activity_service_spec.rb4
-rw-r--r--spec/services/users/build_service_spec.rb2
-rw-r--r--spec/services/users/create_service_spec.rb2
-rw-r--r--spec/services/users/destroy_service_spec.rb10
-rw-r--r--spec/services/users/last_push_event_service_spec.rb2
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb4
-rw-r--r--spec/services/users/refresh_authorized_projects_service_spec.rb2
-rw-r--r--spec/services/users/respond_to_terms_service_spec.rb2
-rw-r--r--spec/services/users/update_service_spec.rb11
-rw-r--r--spec/services/verify_pages_domain_service_spec.rb187
-rw-r--r--spec/services/web_hook_service_spec.rb12
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/destroy_service_spec.rb2
-rw-r--r--spec/services/wiki_pages/update_service_spec.rb2
-rw-r--r--spec/spec_helper.rb20
-rw-r--r--spec/support/api/milestones_shared_examples.rb9
-rw-r--r--spec/support/api/schema_matcher.rb24
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb2
-rw-r--r--spec/support/capybara.rb11
-rw-r--r--spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb6
-rw-r--r--spec/support/database_cleaner.rb56
-rw-r--r--spec/support/db_cleaner.rb50
-rw-r--r--spec/support/external_authorization_service_helpers.rb33
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb6
-rw-r--r--spec/support/features/issuable_quick_actions_shared_examples.rb389
-rw-r--r--spec/support/features/reportable_note_shared_examples.rb6
-rw-r--r--spec/support/features/variable_list_shared_examples.rb163
-rw-r--r--spec/support/helpers/cycle_analytics_helpers.rb2
-rw-r--r--spec/support/helpers/email_helpers.rb4
-rw-r--r--spec/support/helpers/features/notes_helpers.rb10
-rw-r--r--spec/support/helpers/file_mover_helpers.rb12
-rw-r--r--spec/support/helpers/filtered_search_helpers.rb23
-rw-r--r--spec/support/helpers/git_helpers.rb8
-rw-r--r--spec/support/helpers/graphql_helpers.rb75
-rw-r--r--spec/support/helpers/javascript_fixtures_helpers.rb41
-rw-r--r--spec/support/helpers/kubernetes_helpers.rb91
-rw-r--r--spec/support/helpers/lets_encrypt_helpers.rb59
-rw-r--r--spec/support/helpers/license_helper.rb8
-rw-r--r--spec/support/helpers/login_helpers.rb16
-rw-r--r--spec/support/helpers/metrics_dashboard_helpers.rb43
-rw-r--r--spec/support/helpers/mobile_helpers.rb2
-rw-r--r--spec/support/helpers/notification_helpers.rb8
-rw-r--r--spec/support/helpers/policy_helpers.rb11
-rw-r--r--spec/support/helpers/project_forks_helper.rb23
-rw-r--r--spec/support/helpers/prometheus_helpers.rb14
-rw-r--r--spec/support/helpers/query_recorder.rb2
-rw-r--r--spec/support/helpers/repo_helpers.rb92
-rw-r--r--spec/support/helpers/select2_helper.rb4
-rw-r--r--spec/support/helpers/stub_configuration.rb10
-rw-r--r--spec/support/helpers/stub_object_storage.rb15
-rw-r--r--spec/support/helpers/stub_requests.rb40
-rw-r--r--spec/support/helpers/stub_worker.rb9
-rw-r--r--spec/support/helpers/test_env.rb36
-rw-r--r--spec/support/helpers/test_request_helpers.rb4
-rw-r--r--spec/support/helpers/wait_for_requests.rb14
-rw-r--r--spec/support/helpers/wait_helpers.rb20
-rw-r--r--spec/support/import_export/export_file_helper.rb2
-rw-r--r--spec/support/matchers/access_matchers.rb35
-rw-r--r--spec/support/matchers/eq_pem.rb11
-rw-r--r--spec/support/matchers/graphql_matchers.rb4
-rw-r--r--spec/support/matchers/issuable_matchers.rb2
-rw-r--r--spec/support/matchers/not_changed_matcher.rb3
-rw-r--r--spec/support/prometheus/additional_metrics_shared_examples.rb2
-rw-r--r--spec/support/protected_branch_helpers.rb30
-rw-r--r--spec/support/protected_tag_helpers.rb18
-rw-r--r--spec/support/redis/redis_shared_examples.rb2
-rw-r--r--spec/support/services/migrate_to_ghost_user_service_shared_examples.rb4
-rw-r--r--spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb24
-rw-r--r--spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb44
-rw-r--r--spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb65
-rw-r--r--spec/support/shared_contexts/finders/users_finder_shared_contexts.rb8
-rw-r--r--spec/support/shared_contexts/merge_request_create.rb26
-rw-r--r--spec/support/shared_contexts/merge_request_edit.rb28
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb47
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb88
-rw-r--r--spec/support/shared_contexts/services_shared_context.rb8
-rw-r--r--spec/support/shared_examples/application_setting_examples.rb291
-rw-r--r--spec/support/shared_examples/ci/stage_shared_examples.rb27
-rw-r--r--spec/support/shared_examples/ci_trace_shared_examples.rb39
-rw-r--r--spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb40
-rw-r--r--spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb116
-rw-r--r--spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/controllers/variables_shared_examples.rb12
-rw-r--r--spec/support/shared_examples/dirty_submit_form_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb31
-rw-r--r--spec/support/shared_examples/features/editable_merge_request_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb4
-rw-r--r--spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/finders/assignees_filter_shared_examples.rb49
-rw-r--r--spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb30
-rw-r--r--spec/support/shared_examples/helm_generated_script.rb2
-rw-r--r--spec/support/shared_examples/issuable_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/issuables_list_metadata_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/legacy_path_redirect_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/malicious_regexp_shared_examples.rb3
-rw-r--r--spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb (renamed from spec/support/shared_examples/models/atomic_internal_id_spec.rb)42
-rw-r--r--spec/support/shared_examples/models/chat_service_shared_examples.rb (renamed from spec/support/shared_examples/models/chat_service_spec.rb)8
-rw-r--r--spec/support/shared_examples/models/ci_variable_shared_examples.rb29
-rw-r--r--spec/support/shared_examples/models/cluster_application_core_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb30
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb65
-rw-r--r--spec/support/shared_examples/models/member_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/models/update_project_statistics_shared_examples.rb77
-rw-r--r--spec/support/shared_examples/notify_shared_examples.rb73
-rw-r--r--spec/support/shared_examples/policies/project_policy_shared_examples.rb231
-rw-r--r--spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb28
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb110
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb63
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb88
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb88
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb103
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb81
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb83
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb83
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb95
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb82
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb86
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb82
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb86
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb60
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb81
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb84
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb60
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb (renamed from spec/support/shared_examples/time_tracking_shared_examples.rb)15
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb85
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb92
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb120
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb102
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb87
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb86
-rw-r--r--spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb4
-rw-r--r--spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb58
-rw-r--r--spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb38
-rw-r--r--spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb93
-rw-r--r--spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb35
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb51
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb81
-rw-r--r--spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb47
-rw-r--r--spec/support/shared_examples/requests/api/discussions.rb55
-rw-r--r--spec/support/shared_examples/requests/api/issues_shared_examples.rb44
-rw-r--r--spec/support/shared_examples/requests/api/merge_requests_list.rb335
-rw-r--r--spec/support/shared_examples/services/base_helm_service_shared_examples.rb32
-rw-r--r--spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb14
-rw-r--r--spec/support/shared_examples/snippet_visibility.rb322
-rw-r--r--spec/support/shared_examples/snippet_visibility_shared_examples.rb306
-rw-r--r--spec/support/shared_examples/url_validator_examples.rb24
-rw-r--r--spec/support/shared_examples/views/nav_sidebar.rb11
-rw-r--r--spec/support/shared_examples/wiki_file_attachments_examples.rb2
-rw-r--r--spec/support/shoulda/matchers/rails_shim.rb27
-rw-r--r--spec/support/webmock.rb10
-rw-r--r--spec/tasks/gitlab/artifacts/migrate_rake_spec.rb55
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb4
-rw-r--r--spec/tasks/gitlab/shell_rake_spec.rb18
-rw-r--r--spec/tasks/gitlab/storage_rake_spec.rb108
-rw-r--r--spec/tasks/tokens_spec.rb4
-rw-r--r--spec/uploaders/file_mover_spec.rb33
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb32
-rw-r--r--spec/uploaders/legacy_artifact_uploader_spec.rb61
-rw-r--r--spec/uploaders/object_storage_spec.rb12
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb60
-rw-r--r--spec/uploaders/records_uploads_spec.rb13
-rw-r--r--spec/uploaders/workers/object_storage/background_move_worker_spec.rb34
-rw-r--r--spec/validators/addressable_url_validator_spec.rb (renamed from spec/validators/url_validator_spec.rb)131
-rw-r--r--spec/validators/devise_email_validator_spec.rb94
-rw-r--r--spec/validators/public_url_validator_spec.rb8
-rw-r--r--spec/validators/sha_validator_spec.rb47
-rw-r--r--spec/validators/x509_certificate_credentials_validator_spec.rb87
-rw-r--r--spec/views/ci/status/_icon.html.haml_spec.rb89
-rw-r--r--spec/views/groups/_home_panel.html.haml_spec.rb15
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb2
-rw-r--r--spec/views/help/index.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb2
-rw-r--r--spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb13
-rw-r--r--spec/views/layouts/nav/sidebar/_instance_statistics.html.haml_spec.rb7
-rw-r--r--spec/views/layouts/nav/sidebar/_profile.html.haml_spec.rb13
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb54
-rw-r--r--spec/views/notify/pipeline_failed_email.text.erb_spec.rb39
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb4
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb4
-rw-r--r--spec/views/projects/deployments/_confirm_rollback_modal_spec.html.rb63
-rw-r--r--spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb22
-rw-r--r--spec/views/projects/issues/show.html.haml_spec.rb27
-rw-r--r--spec/views/projects/jobs/_build.html.haml_spec.rb10
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb6
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb13
-rw-r--r--spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb8
-rw-r--r--spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb11
-rw-r--r--spec/views/projects/settings/operations/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tags/index.html.haml_spec.rb41
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/views/shared/_label_row.html.haml.rb32
-rw-r--r--spec/views/shared/milestones/_issuables.html.haml.rb6
-rw-r--r--spec/views/shared/projects/_project.html.haml_spec.rb4
-rw-r--r--spec/workers/admin_email_worker_spec.rb2
-rw-r--r--spec/workers/archive_trace_worker_spec.rb2
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb2
-rw-r--r--spec/workers/auto_merge_process_worker_spec.rb31
-rw-r--r--spec/workers/background_migration_worker_spec.rb2
-rw-r--r--spec/workers/build_coverage_worker_spec.rb2
-rw-r--r--spec/workers/build_finished_worker_spec.rb3
-rw-r--r--spec/workers/build_hooks_worker_spec.rb2
-rw-r--r--spec/workers/build_success_worker_spec.rb3
-rw-r--r--spec/workers/build_trace_sections_worker_spec.rb2
-rw-r--r--spec/workers/ci/archive_traces_cron_worker_spec.rb2
-rw-r--r--spec/workers/ci/build_prepare_worker_spec.rb30
-rw-r--r--spec/workers/ci/build_schedule_worker_spec.rb2
-rw-r--r--spec/workers/cluster_configure_worker_spec.rb56
-rw-r--r--spec/workers/cluster_project_configure_worker_spec.rb14
-rw-r--r--spec/workers/cluster_provision_worker_spec.rb2
-rw-r--r--spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb2
-rw-r--r--spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb32
-rw-r--r--spec/workers/concerns/application_worker_spec.rb2
-rw-r--r--spec/workers/concerns/cluster_queue_spec.rb2
-rw-r--r--spec/workers/concerns/cronjob_queue_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/github_import/queue_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb2
-rw-r--r--spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb2
-rw-r--r--spec/workers/concerns/pipeline_background_queue_spec.rb2
-rw-r--r--spec/workers/concerns/pipeline_queue_spec.rb2
-rw-r--r--spec/workers/concerns/project_import_options_spec.rb2
-rw-r--r--spec/workers/concerns/repository_check_queue_spec.rb2
-rw-r--r--spec/workers/concerns/waitable_worker_spec.rb2
-rw-r--r--spec/workers/create_gpg_signature_worker_spec.rb2
-rw-r--r--spec/workers/create_note_diff_file_worker_spec.rb2
-rw-r--r--spec/workers/create_pipeline_worker_spec.rb2
-rw-r--r--spec/workers/delete_diff_files_worker_spec.rb2
-rw-r--r--spec/workers/delete_merged_branches_worker_spec.rb2
-rw-r--r--spec/workers/delete_user_worker_spec.rb2
-rw-r--r--spec/workers/deployments/finished_worker_spec.rb39
-rw-r--r--spec/workers/deployments/success_worker_spec.rb2
-rw-r--r--spec/workers/detect_repository_languages_worker_spec.rb13
-rw-r--r--spec/workers/email_receiver_worker_spec.rb2
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb2
-rw-r--r--spec/workers/every_sidekiq_worker_spec.rb2
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb2
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb8
-rw-r--r--spec/workers/expire_job_cache_worker_spec.rb2
-rw-r--r--spec/workers/expire_pipeline_cache_worker_spec.rb31
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb15
-rw-r--r--spec/workers/gitlab/github_import/advance_stage_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_issue_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb2
-rw-r--r--spec/workers/gitlab_shell_worker_spec.rb2
-rw-r--r--spec/workers/gitlab_usage_ping_worker_spec.rb2
-rw-r--r--spec/workers/group_destroy_worker_spec.rb2
-rw-r--r--spec/workers/hashed_storage/migrator_worker_spec.rb2
-rw-r--r--spec/workers/hashed_storage/project_migrate_worker_spec.rb (renamed from spec/workers/project_migrate_hashed_storage_worker_spec.rb)4
-rw-r--r--spec/workers/hashed_storage/project_rollback_worker_spec.rb50
-rw-r--r--spec/workers/hashed_storage/rollbacker_worker_spec.rb27
-rw-r--r--spec/workers/invalid_gpg_signature_update_worker_spec.rb2
-rw-r--r--spec/workers/issue_due_scheduler_worker_spec.rb2
-rw-r--r--spec/workers/mail_scheduler/issue_due_worker_spec.rb2
-rw-r--r--spec/workers/mail_scheduler/notification_service_worker_spec.rb2
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--spec/workers/migrate_external_diffs_worker_spec.rb25
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb2
-rw-r--r--spec/workers/new_issue_worker_spec.rb2
-rw-r--r--spec/workers/new_merge_request_worker_spec.rb2
-rw-r--r--spec/workers/new_note_worker_spec.rb2
-rw-r--r--spec/workers/pages_domain_removal_cron_worker_spec.rb27
-rw-r--r--spec/workers/pages_domain_verification_cron_worker_spec.rb17
-rw-r--r--spec/workers/pages_domain_verification_worker_spec.rb9
-rw-r--r--spec/workers/pipeline_hooks_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_metrics_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_notification_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_process_worker_spec.rb2
-rw-r--r--spec/workers/pipeline_schedule_worker_spec.rb51
-rw-r--r--spec/workers/pipeline_success_worker_spec.rb24
-rw-r--r--spec/workers/pipeline_update_worker_spec.rb2
-rw-r--r--spec/workers/plugin_worker_spec.rb2
-rw-r--r--spec/workers/post_receive_spec.rb81
-rw-r--r--spec/workers/process_commit_worker_spec.rb2
-rw-r--r--spec/workers/project_cache_worker_spec.rb54
-rw-r--r--spec/workers/project_daily_statistics_worker_spec.rb35
-rw-r--r--spec/workers/project_destroy_worker_spec.rb2
-rw-r--r--spec/workers/project_export_worker_spec.rb2
-rw-r--r--spec/workers/propagate_service_template_worker_spec.rb2
-rw-r--r--spec/workers/prune_old_events_worker_spec.rb2
-rw-r--r--spec/workers/prune_web_hook_logs_worker_spec.rb2
-rw-r--r--spec/workers/reactive_caching_worker_spec.rb2
-rw-r--r--spec/workers/rebase_worker_spec.rb2
-rw-r--r--spec/workers/remote_mirror_notification_worker_spec.rb2
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb2
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb2
-rw-r--r--spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/batch_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/clear_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/dispatch_worker_spec.rb2
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb2
-rw-r--r--spec/workers/repository_cleanup_worker_spec.rb2
-rw-r--r--spec/workers/repository_fork_worker_spec.rb2
-rw-r--r--spec/workers/repository_import_worker_spec.rb2
-rw-r--r--spec/workers/repository_remove_remote_worker_spec.rb2
-rw-r--r--spec/workers/repository_update_remote_mirror_worker_spec.rb2
-rw-r--r--spec/workers/run_pipeline_schedule_worker_spec.rb34
-rw-r--r--spec/workers/schedule_migrate_external_diffs_worker_spec.rb25
-rw-r--r--spec/workers/stage_update_worker_spec.rb2
-rw-r--r--spec/workers/stuck_ci_jobs_worker_spec.rb2
-rw-r--r--spec/workers/stuck_import_jobs_worker_spec.rb2
-rw-r--r--spec/workers/stuck_merge_jobs_worker_spec.rb2
-rw-r--r--spec/workers/system_hook_push_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/confidential_issue_worker_spec.rb15
-rw-r--r--spec/workers/todos_destroyer/entity_leave_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/group_private_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/private_features_worker_spec.rb2
-rw-r--r--spec/workers/todos_destroyer/project_private_worker_spec.rb2
-rw-r--r--spec/workers/trending_projects_worker_spec.rb2
-rw-r--r--spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb6
-rw-r--r--spec/workers/update_merge_requests_worker_spec.rb2
-rw-r--r--spec/workers/update_project_statistics_worker_spec.rb17
-rw-r--r--spec/workers/upload_checksum_worker_spec.rb2
-rw-r--r--spec/workers/wait_for_cluster_creation_worker_spec.rb2
2391 files changed, 71251 insertions, 25007 deletions
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index b2c2fb810e8..84bbe0525e5 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
load File.expand_path('../../bin/changelog', __dir__)
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index 74634dac713..94b29b89f24 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'mail_room.yml' do
diff --git a/spec/config/object_store_settings_spec.rb b/spec/config/object_store_settings_spec.rb
index b1ada3c99db..c38910cff0a 100644
--- a/spec/config/object_store_settings_spec.rb
+++ b/spec/config/object_store_settings_spec.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require Rails.root.join('config', 'object_store_settings.rb')
describe ObjectStoreSettings do
describe '.parse' do
- it 'should set correct default values' do
+ it 'sets correct default values' do
settings = described_class.parse(nil)
expect(settings['enabled']).to be false
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb
index c89b5f48dc0..26d92593a08 100644
--- a/spec/config/settings_spec.rb
+++ b/spec/config/settings_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Settings do
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
index 7104305e9d2..e360ab68cf2 100644
--- a/spec/controllers/abuse_reports_controller_spec.rb
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AbuseReportsController do
diff --git a/spec/controllers/acme_challenges_controller_spec.rb b/spec/controllers/acme_challenges_controller_spec.rb
new file mode 100644
index 00000000000..cee06bed27b
--- /dev/null
+++ b/spec/controllers/acme_challenges_controller_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AcmeChallengesController do
+ describe '#show' do
+ let!(:acme_order) { create(:pages_domain_acme_order) }
+
+ def make_request(domain, token)
+ get(:show, params: { domain: domain, token: token })
+ end
+
+ before do
+ make_request(domain, token)
+ end
+
+ context 'with right domain and token' do
+ let(:domain) { acme_order.pages_domain.domain }
+ let(:token) { acme_order.challenge_token }
+
+ it 'renders acme challenge file content' do
+ expect(response.body).to eq(acme_order.challenge_file_content)
+ end
+ end
+
+ context 'when domain is invalid' do
+ let(:domain) { 'somewrongdomain.com' }
+ let(:token) { acme_order.challenge_token }
+
+ it 'renders not found' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when token is invalid' do
+ let(:domain) { acme_order.pages_domain.domain }
+ let(:token) { 'wrongtoken' }
+
+ it 'renders not found' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/admin/appearances_controller_spec.rb b/spec/controllers/admin/appearances_controller_spec.rb
index 4ddd0953267..621aa148301 100644
--- a/spec/controllers/admin/appearances_controller_spec.rb
+++ b/spec/controllers/admin/appearances_controller_spec.rb
@@ -1,15 +1,17 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::AppearancesController do
let(:admin) { create(:admin) }
- let(:header_message) { "Header message" }
- let(:footer_message) { "Footer" }
+ let(:header_message) { 'Header message' }
+ let(:footer_message) { 'Footer' }
describe 'POST #create' do
let(:create_params) do
{
- title: "Foo",
- description: "Bar",
+ title: 'Foo',
+ description: 'Bar',
header_message: header_message,
footer_message: footer_message
}
@@ -24,9 +26,26 @@ describe Admin::AppearancesController do
expect(Appearance.current).to have_attributes(
header_message: header_message,
- footer_message: footer_message
+ footer_message: footer_message,
+ email_header_and_footer_enabled: false,
+ message_background_color: '#E75E40',
+ message_font_color: '#FFFFFF'
)
end
+
+ context 'when enabling header and footer in email' do
+ it 'creates appearance with enabled flag' do
+ create_params[:email_header_and_footer_enabled] = true
+
+ post :create, params: { appearance: create_params }
+
+ expect(Appearance.current).to have_attributes(
+ header_message: header_message,
+ footer_message: footer_message,
+ email_header_and_footer_enabled: true
+ )
+ end
+ end
end
describe 'PUT #update' do
@@ -48,8 +67,25 @@ describe Admin::AppearancesController do
expect(Appearance.current).to have_attributes(
header_message: header_message,
- footer_message: footer_message
+ footer_message: footer_message,
+ email_header_and_footer_enabled: false,
+ message_background_color: '#E75E40',
+ message_font_color: '#FFFFFF'
)
end
+
+ context 'when enabling header and footer in email' do
+ it 'updates appearance with enabled flag' do
+ update_params[:email_header_and_footer_enabled] = true
+
+ post :update, params: { appearance: update_params }
+
+ expect(Appearance.current).to have_attributes(
+ header_message: header_message,
+ footer_message: footer_message,
+ email_header_and_footer_enabled: true
+ )
+ end
+ end
end
end
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
index 9af472df74e..b89348b7a7e 100644
--- a/spec/controllers/admin/application_settings_controller_spec.rb
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::ApplicationSettingsController do
@@ -85,6 +87,35 @@ describe Admin::ApplicationSettingsController do
expect(response).to redirect_to(admin_application_settings_path)
expect(ApplicationSetting.current.receive_max_input_size).to eq(1024)
end
+
+ it 'updates the default_project_creation for string value' do
+ put :update, params: { application_setting: { default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } }
+
+ expect(response).to redirect_to(admin_application_settings_path)
+ expect(ApplicationSetting.current.default_project_creation).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ end
+
+ context 'external policy classification settings' do
+ let(:settings) do
+ {
+ external_authorization_service_enabled: true,
+ external_authorization_service_url: 'https://custom.service/',
+ external_authorization_service_default_label: 'default',
+ external_authorization_service_timeout: 3,
+ external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'),
+ external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'),
+ external_auth_client_key_pass: "5iveL!fe"
+ }
+ end
+
+ it 'updates settings when the feature is available' do
+ put :update, params: { application_setting: settings }
+
+ settings.each do |attribute, value|
+ expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
+ end
+ end
+ end
end
describe 'PUT #reset_registration_token' do
diff --git a/spec/controllers/admin/applications_controller_spec.rb b/spec/controllers/admin/applications_controller_spec.rb
index 7e1ce70dc7d..9c9f658a0bd 100644
--- a/spec/controllers/admin/applications_controller_spec.rb
+++ b/spec/controllers/admin/applications_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::ApplicationsController do
diff --git a/spec/controllers/admin/clusters/applications_controller_spec.rb b/spec/controllers/admin/clusters/applications_controller_spec.rb
new file mode 100644
index 00000000000..76f261e7d3f
--- /dev/null
+++ b/spec/controllers/admin/clusters/applications_controller_spec.rb
@@ -0,0 +1,149 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::Clusters::ApplicationsController do
+ include AccessMatchersForController
+
+ def current_application
+ Clusters::Cluster::APPLICATIONS[application]
+ end
+
+ shared_examples 'a secure endpoint' do
+ it { expect { subject }.to be_allowed_for(:admin) }
+ it { expect { subject }.to be_denied_for(:user) }
+ it { expect { subject }.to be_denied_for(:external) }
+
+ context 'when instance clusters are disabled' do
+ before do
+ stub_feature_flags(instance_clusters: false)
+ end
+
+ it 'returns 404' do
+ is_expected.to have_http_status(:not_found)
+ end
+ end
+ end
+
+ let(:cluster) { create(:cluster, :instance, :provided_by_gcp) }
+
+ describe 'POST create' do
+ subject do
+ post :create, params: params
+ end
+
+ let(:application) { 'helm' }
+ let(:params) { { application: application, id: cluster.id } }
+
+ describe 'functionality' do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'schedule an application installation' do
+ expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
+
+ expect { subject }.to change { current_application.count }
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.application_helm).to be_scheduled
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it 'return 404' do
+ expect { subject }.not_to change { current_application.count }
+ expect(response).to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is unknown' do
+ let(:application) { 'unkwnown-app' }
+
+ it 'return 404' do
+ is_expected.to have_http_status(:not_found)
+ end
+ end
+
+ context 'when application is already installing' do
+ before do
+ create(:clusters_applications_helm, :installing, cluster: cluster)
+ end
+
+ it 'returns 400' do
+ is_expected.to have_http_status(:bad_request)
+ end
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
+ end
+ end
+
+ describe 'PATCH update' do
+ subject do
+ patch :update, params: params
+ end
+
+ let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) }
+ let(:application_name) { application.name }
+ let(:params) { { application: application_name, id: cluster.id, email: "new-email@example.com" } }
+
+ describe 'functionality' do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ context "when cluster and app exists" do
+ it "schedules an application update" do
+ expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once
+
+ is_expected.to have_http_status(:no_content)
+
+ expect(cluster.application_cert_manager).to be_scheduled
+ end
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is unknown' do
+ let(:application_name) { 'unkwnown-app' }
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is already scheduled' do
+ before do
+ application.make_scheduled!
+ end
+
+ it { is_expected.to have_http_status(:bad_request) }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterPatchAppWorker).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
+ end
+ end
+end
diff --git a/spec/controllers/admin/clusters_controller_spec.rb b/spec/controllers/admin/clusters_controller_spec.rb
new file mode 100644
index 00000000000..7b77cb186a4
--- /dev/null
+++ b/spec/controllers/admin/clusters_controller_spec.rb
@@ -0,0 +1,540 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Admin::ClustersController do
+ include AccessMatchersForController
+ include GoogleApi::CloudPlatformHelpers
+
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'GET #index' do
+ def get_index(params = {})
+ get :index, params: params
+ end
+
+ context 'when feature flag is not enabled' do
+ before do
+ stub_feature_flags(instance_clusters: false)
+ end
+
+ it 'responds with not found' do
+ get_index
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(instance_clusters: true)
+ end
+
+ describe 'functionality' do
+ context 'when instance has one or more clusters' do
+ let!(:enabled_cluster) do
+ create(:cluster, :provided_by_gcp, :instance)
+ end
+
+ let!(:disabled_cluster) do
+ create(:cluster, :disabled, :provided_by_gcp, :production_environment, :instance)
+ end
+
+ it 'lists available clusters' do
+ get_index
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index)
+ expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
+ end
+
+ context 'when page is specified' do
+ let(:last_page) { Clusters::Cluster.instance_type.page.total_pages }
+
+ before do
+ allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
+ create_list(:cluster, 2, :provided_by_gcp, :production_environment, :instance)
+ end
+
+ it 'redirects to the page' do
+ get_index(page: last_page)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:clusters).current_page).to eq(last_page)
+ end
+ end
+ end
+
+ context 'when instance does not have a cluster' do
+ it 'returns an empty state page' do
+ get_index
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:index, partial: :empty_state)
+ expect(assigns(:clusters)).to eq([])
+ end
+ end
+ end
+ end
+
+ describe 'security' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { expect { get_index }.to be_allowed_for(:admin) }
+ it { expect { get_index }.to be_denied_for(:user) }
+ it { expect { get_index }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET #new' do
+ def get_new
+ get :new
+ end
+
+ describe 'functionality for new cluster' do
+ context 'when omniauth has been configured' do
+ let(:key) { 'secret-key' }
+ let(:session_key_for_redirect_uri) do
+ GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(key)
+ end
+
+ before do
+ allow(SecureRandom).to receive(:hex).and_return(key)
+ end
+
+ it 'has authorize_url' do
+ get_new
+
+ expect(assigns(:authorize_url)).to include(key)
+ expect(session[session_key_for_redirect_uri]).to eq(new_admin_cluster_path)
+ end
+ end
+
+ context 'when omniauth has not configured' do
+ before do
+ stub_omniauth_setting(providers: [])
+ end
+
+ it 'does not have authorize_url' do
+ get_new
+
+ expect(assigns(:authorize_url)).to be_nil
+ end
+ end
+
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'has new object' do
+ get_new
+
+ expect(assigns(:gcp_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'functionality for existing cluster' do
+ it 'has new object' do
+ get_new
+
+ expect(assigns(:user_cluster)).to be_an_instance_of(Clusters::ClusterPresenter)
+ end
+ end
+
+ describe 'security' do
+ it { expect { get_new }.to be_allowed_for(:admin) }
+ it { expect { get_new }.to be_denied_for(:user) }
+ it { expect { get_new }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST #create_gcp' do
+ let(:legacy_abac_param) { 'true' }
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project-12345',
+ legacy_abac: legacy_abac_param
+ }
+ }
+ }
+ end
+
+ def post_create_gcp
+ post :create_gcp, params: params
+ end
+
+ describe 'functionality' do
+ context 'when access token is valid' do
+ before do
+ stub_google_api_validate_token
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { post_create_gcp }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+
+ cluster = Clusters::Cluster.instance_type.first
+
+ expect(response).to redirect_to(admin_cluster_path(cluster))
+ expect(cluster).to be_gcp
+ expect(cluster).to be_kubernetes
+ expect(cluster.provider_gcp).to be_legacy_abac
+ end
+
+ context 'when legacy_abac param is false' do
+ let(:legacy_abac_param) { 'false' }
+
+ it 'creates a new cluster with legacy_abac_disabled' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { post_create_gcp }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(Clusters::Cluster.instance_type.first.provider_gcp).not_to be_legacy_abac
+ end
+ end
+ end
+
+ context 'when access token is expired' do
+ before do
+ stub_google_api_expired_token
+ end
+
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+
+ context 'when access token is not stored in session' do
+ it { expect(@valid_gcp_token).to be_falsey }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow_any_instance_of(described_class)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(described_class)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+ end
+
+ it { expect { post_create_gcp }.to be_allowed_for(:admin) }
+ it { expect { post_create_gcp }.to be_denied_for(:user) }
+ it { expect { post_create_gcp }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'POST #create_user' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test'
+ }
+ }
+ }
+ end
+
+ def post_create_user
+ post :create_user, params: params
+ end
+
+ describe 'functionality' do
+ context 'when creates a cluster' do
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { post_create_user }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = Clusters::Cluster.instance_type.first
+
+ expect(response).to redirect_to(admin_cluster_path(cluster))
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ end
+ end
+
+ context 'when creates a RBAC-enabled cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ authorization_type: 'rbac'
+ }
+ }
+ }
+ end
+
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+
+ expect { post_create_user }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Platforms::Kubernetes.count }
+
+ cluster = Clusters::Cluster.instance_type.first
+
+ expect(response).to redirect_to(admin_cluster_path(cluster))
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ expect(cluster).to be_platform_kubernetes_rbac
+ end
+ end
+ end
+
+ describe 'security' do
+ it { expect { post_create_user }.to be_allowed_for(:admin) }
+ it { expect { post_create_user }.to be_denied_for(:user) }
+ it { expect { post_create_user }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET #cluster_status' do
+ let(:cluster) { create(:cluster, :providing_by_gcp, :instance) }
+
+ def get_cluster_status
+ get :cluster_status,
+ params: {
+ id: cluster
+ },
+ format: :json
+ end
+
+ describe 'functionality' do
+ it 'responds with matching schema' do
+ get_cluster_status
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('cluster_status')
+ end
+
+ it 'invokes schedule_status_update on each application' do
+ expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
+
+ get_cluster_status
+ end
+ end
+
+ describe 'security' do
+ it { expect { get_cluster_status }.to be_allowed_for(:admin) }
+ it { expect { get_cluster_status }.to be_denied_for(:user) }
+ it { expect { get_cluster_status }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'GET #show' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ def get_show
+ get :show,
+ params: {
+ id: cluster
+ }
+ end
+
+ describe 'functionality' do
+ it 'responds successfully' do
+ get_show
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(assigns(:cluster)).to eq(cluster)
+ end
+ end
+
+ describe 'security' do
+ it { expect { get_show }.to be_allowed_for(:admin) }
+ it { expect { get_show }.to be_denied_for(:user) }
+ it { expect { get_show }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'PUT #update' do
+ def put_update(format: :html)
+ put :update, params: params.merge(
+ id: cluster,
+ format: format
+ )
+ end
+
+ let(:cluster) { create(:cluster, :provided_by_user, :instance) }
+ let(:domain) { 'test-domain.com' }
+
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ base_domain: domain
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ put_update
+
+ cluster.reload
+ expect(response).to redirect_to(admin_cluster_path(cluster))
+ expect(flash[:notice]).to eq('Kubernetes cluster was successfully updated.')
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ expect(cluster.domain).to eq('test-domain.com')
+ end
+
+ context 'when domain is invalid' do
+ let(:domain) { 'http://not-a-valid-domain' }
+
+ it 'does not update cluster attributes' do
+ put_update
+
+ cluster.reload
+ expect(response).to render_template(:show)
+ expect(cluster.name).not_to eq('my-new-cluster-name')
+ expect(cluster.domain).not_to eq('test-domain.com')
+ end
+ end
+
+ context 'when format is json' do
+ context 'when changing parameters' do
+ context 'when valid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: 'my-new-cluster-name',
+ domain: domain
+ }
+ }
+ end
+
+ it 'updates and redirects back to show page' do
+ put_update(format: :json)
+
+ cluster.reload
+ expect(response).to have_http_status(:no_content)
+ expect(cluster.enabled).to be_falsey
+ expect(cluster.name).to eq('my-new-cluster-name')
+ end
+ end
+
+ context 'when invalid parameters are used' do
+ let(:params) do
+ {
+ cluster: {
+ enabled: false,
+ name: ''
+ }
+ }
+ end
+
+ it 'rejects changes' do
+ put_update(format: :json)
+
+ expect(response).to have_http_status(:bad_request)
+ end
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { expect { put_update }.to be_allowed_for(:admin) }
+ it { expect { put_update }.to be_denied_for(:user) }
+ it { expect { put_update }.to be_denied_for(:external) }
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ let!(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) }
+
+ def delete_destroy
+ delete :destroy,
+ params: {
+ id: cluster
+ }
+ end
+
+ describe 'functionality' do
+ context 'when cluster is provided by GCP' do
+ context 'when cluster is created' do
+ it 'destroys and redirects back to clusters list' do
+ expect { delete_destroy }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(admin_clusters_path)
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+
+ context 'when cluster is being created' do
+ let!(:cluster) { create(:cluster, :providing_by_gcp, :production_environment, :instance) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { delete_destroy }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(-1)
+
+ expect(response).to redirect_to(admin_clusters_path)
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+ end
+
+ context 'when cluster is provided by user' do
+ let!(:cluster) { create(:cluster, :provided_by_user, :production_environment, :instance) }
+
+ it 'destroys and redirects back to clusters list' do
+ expect { delete_destroy }
+ .to change { Clusters::Cluster.count }.by(-1)
+ .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
+ .and change { Clusters::Providers::Gcp.count }.by(0)
+
+ expect(response).to redirect_to(admin_clusters_path)
+ expect(flash[:notice]).to eq('Kubernetes cluster integration was successfully removed.')
+ end
+ end
+ end
+
+ describe 'security' do
+ set(:cluster) { create(:cluster, :provided_by_gcp, :production_environment, :instance) }
+
+ it { expect { delete_destroy }.to be_allowed_for(:admin) }
+ it { expect { delete_destroy }.to be_denied_for(:user) }
+ it { expect { delete_destroy }.to be_denied_for(:external) }
+ end
+ end
+end
diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/controllers/admin/dashboard_controller_spec.rb
index 6eb9f7867d5..4de69a9aea1 100644
--- a/spec/controllers/admin/dashboard_controller_spec.rb
+++ b/spec/controllers/admin/dashboard_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::DashboardController do
diff --git a/spec/controllers/admin/gitaly_servers_controller_spec.rb b/spec/controllers/admin/gitaly_servers_controller_spec.rb
index b7378aa37d0..c75418a9ad4 100644
--- a/spec/controllers/admin/gitaly_servers_controller_spec.rb
+++ b/spec/controllers/admin/gitaly_servers_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::GitalyServersController do
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 647fce0ecef..509d8944e3a 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::GroupsController do
@@ -60,5 +62,11 @@ describe Admin::GroupsController do
expect(response).to redirect_to(admin_group_path(group))
expect(group.users).not_to include group_user
end
+
+ it 'updates the project_creation_level successfully' do
+ expect do
+ post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS } }
+ end.to change { group.reload.project_creation_level }.to(::Gitlab::Access::NO_ONE_PROJECT_ACCESS)
+ end
end
end
diff --git a/spec/controllers/admin/health_check_controller_spec.rb b/spec/controllers/admin/health_check_controller_spec.rb
index e13401fc06b..cf5b27156c0 100644
--- a/spec/controllers/admin/health_check_controller_spec.rb
+++ b/spec/controllers/admin/health_check_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::HealthCheckController do
diff --git a/spec/controllers/admin/hooks_controller_spec.rb b/spec/controllers/admin/hooks_controller_spec.rb
index 9bc58344e4e..3c3a16ef9d5 100644
--- a/spec/controllers/admin/hooks_controller_spec.rb
+++ b/spec/controllers/admin/hooks_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::HooksController do
diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb
index e5428c8ddeb..68695afdb61 100644
--- a/spec/controllers/admin/identities_controller_spec.rb
+++ b/spec/controllers/admin/identities_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::IdentitiesController do
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index 944680b3f42..b44797b23e5 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::ImpersonationsController do
diff --git a/spec/controllers/admin/projects_controller_spec.rb b/spec/controllers/admin/projects_controller_spec.rb
index 8166657f674..6b996798b74 100644
--- a/spec/controllers/admin/projects_controller_spec.rb
+++ b/spec/controllers/admin/projects_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::ProjectsController do
@@ -43,6 +45,16 @@ describe Admin::ProjectsController do
end
end
+ describe 'GET /projects.json' do
+ render_views
+
+ before do
+ get :index, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
+ end
+
describe 'GET /projects/:id' do
render_views
diff --git a/spec/controllers/admin/runners_controller_spec.rb b/spec/controllers/admin/runners_controller_spec.rb
index 4cf14030ca1..78c5e2a2656 100644
--- a/spec/controllers/admin/runners_controller_spec.rb
+++ b/spec/controllers/admin/runners_controller_spec.rb
@@ -1,18 +1,37 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::RunnersController do
- let(:runner) { create(:ci_runner) }
+ let!(:runner) { create(:ci_runner) }
before do
sign_in(create(:admin))
end
describe '#index' do
+ render_views
+
it 'lists all runners' do
get :index
expect(response).to have_gitlab_http_status(200)
end
+
+ it 'avoids N+1 queries', :request_store do
+ get :index
+
+ control_count = ActiveRecord::QueryRecorder.new { get :index }.count
+
+ create(:ci_runner, :tagged_only)
+
+ # There is still an N+1 query for `runner.builds.count`
+ expect { get :index }.not_to exceed_query_limit(control_count + 1)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to have_content('tag1')
+ expect(response.body).to have_content('tag2')
+ end
end
describe '#show' do
diff --git a/spec/controllers/admin/services_controller_spec.rb b/spec/controllers/admin/services_controller_spec.rb
index ec161b92245..1c518dab11e 100644
--- a/spec/controllers/admin/services_controller_spec.rb
+++ b/spec/controllers/admin/services_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::ServicesController do
diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb
index 2b946ec1c68..3bc49023357 100644
--- a/spec/controllers/admin/spam_logs_controller_spec.rb
+++ b/spec/controllers/admin/spam_logs_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::SpamLogsController do
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index cb24a6ef142..89a0eba66f7 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Admin::UsersController do
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb
index c9e520317e8..40669ec5451 100644
--- a/spec/controllers/application_controller_spec.rb
+++ b/spec/controllers/application_controller_spec.rb
@@ -1,4 +1,5 @@
# coding: utf-8
+# frozen_string_literal: true
require 'spec_helper'
describe ApplicationController do
@@ -205,8 +206,19 @@ describe ApplicationController do
describe '#check_two_factor_requirement' do
subject { controller.send :check_two_factor_requirement }
+ it 'does not redirect if user has temporary oauth email' do
+ oauth_user = create(:user, email: 'temp-email-for-oauth@email.com')
+ allow(controller).to receive(:two_factor_authentication_required?).and_return(true)
+ allow(controller).to receive(:current_user).and_return(oauth_user)
+
+ expect(controller).not_to receive(:redirect_to)
+
+ subject
+ end
+
it 'does not redirect if 2FA is not required' do
allow(controller).to receive(:two_factor_authentication_required?).and_return(false)
+
expect(controller).not_to receive(:redirect_to)
subject
@@ -215,6 +227,7 @@ describe ApplicationController do
it 'does not redirect if user is not logged in' do
allow(controller).to receive(:two_factor_authentication_required?).and_return(true)
allow(controller).to receive(:current_user).and_return(nil)
+
expect(controller).not_to receive(:redirect_to)
subject
@@ -222,8 +235,9 @@ describe ApplicationController do
it 'does not redirect if user has 2FA enabled' do
allow(controller).to receive(:two_factor_authentication_required?).and_return(true)
- allow(controller).to receive(:current_user).twice.and_return(user)
+ allow(controller).to receive(:current_user).thrice.and_return(user)
allow(user).to receive(:two_factor_enabled?).and_return(true)
+
expect(controller).not_to receive(:redirect_to)
subject
@@ -231,9 +245,10 @@ describe ApplicationController do
it 'does not redirect if 2FA setup can be skipped' do
allow(controller).to receive(:two_factor_authentication_required?).and_return(true)
- allow(controller).to receive(:current_user).twice.and_return(user)
+ allow(controller).to receive(:current_user).thrice.and_return(user)
allow(user).to receive(:two_factor_enabled?).and_return(false)
allow(controller).to receive(:skip_two_factor?).and_return(true)
+
expect(controller).not_to receive(:redirect_to)
subject
@@ -241,10 +256,11 @@ describe ApplicationController do
it 'redirects to 2FA setup otherwise' do
allow(controller).to receive(:two_factor_authentication_required?).and_return(true)
- allow(controller).to receive(:current_user).twice.and_return(user)
+ allow(controller).to receive(:current_user).thrice.and_return(user)
allow(user).to receive(:two_factor_enabled?).and_return(false)
allow(controller).to receive(:skip_two_factor?).and_return(false)
allow(controller).to receive(:profile_two_factor_auth_path)
+
expect(controller).to receive(:redirect_to)
subject
@@ -461,7 +477,7 @@ describe ApplicationController do
end
it 'does log correlation id' do
- Gitlab::CorrelationId.use_id('new-id') do
+ Labkit::Correlation::CorrelationId.use_id('new-id') do
get :index
end
@@ -665,6 +681,48 @@ describe ApplicationController do
expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate, no-store'
end
+
+ it 'does not set the "no-store" header for XHR requests' do
+ sign_in(user)
+
+ get :index, xhr: true
+
+ expect(response.headers['Cache-Control']).to eq 'max-age=0, private, must-revalidate'
+ end
+ end
+ end
+
+ context 'Gitlab::Session' do
+ controller(described_class) do
+ prepend_before_action do
+ authenticate_sessionless_user!(:rss)
+ end
+
+ def index
+ if Gitlab::Session.current
+ head :created
+ else
+ head :not_found
+ end
+ end
+ end
+
+ it 'is set on web requests' do
+ sign_in(user)
+
+ get :index
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ context 'with sessionless user' do
+ it 'is not set' do
+ personal_access_token = create(:personal_access_token, user: user)
+
+ get :index, format: :atom, params: { private_token: personal_access_token.token }
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
end
end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 4458a7223bf..3f1c0ae8ac4 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AutocompleteController do
@@ -371,5 +373,36 @@ describe AutocompleteController do
expect(json_response[3]).to match('name' => 'thumbsdown')
end
end
+
+ context 'Get merge_request_target_branches' do
+ let(:user2) { create(:user) }
+ let!(:merge_request1) { create(:merge_request, source_project: project, target_branch: 'feature') }
+
+ context 'unauthorized user' do
+ it 'returns empty json' do
+ get :merge_request_target_branches
+
+ expect(json_response).to be_empty
+ end
+ end
+
+ context 'sign in as user without any accesible merge requests' do
+ it 'returns empty json' do
+ sign_in(user2)
+ get :merge_request_target_branches
+
+ expect(json_response).to be_empty
+ end
+ end
+
+ context 'sign in as user with a accesible merge request' do
+ it 'returns json' do
+ sign_in(user)
+ get :merge_request_target_branches
+
+ expect(json_response).to contain_exactly({ 'title' => 'feature' })
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb
index 5eb05f01b8d..c84bb913cad 100644
--- a/spec/controllers/boards/issues_controller_spec.rb
+++ b/spec/controllers/boards/issues_controller_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::IssuesController do
+ include ExternalAuthorizationServiceHelpers
+
let(:project) { create(:project, :private) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
@@ -136,6 +140,30 @@ describe Boards::IssuesController do
end
end
+ context 'with external authorization' do
+ before do
+ sign_in(user)
+ enable_external_authorization_service_check
+ end
+
+ it 'returns a 403 for group boards' do
+ group = create(:group)
+ group_board = create(:board, group: group)
+
+ list_issues(user: user, board: group_board)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'is successful for project boards' do
+ project_board = create(:board, project: project)
+
+ list_issues(user: user, board: project_board)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
def list_issues(user:, board:, list: nil)
sign_in(user)
diff --git a/spec/controllers/boards/lists_controller_spec.rb b/spec/controllers/boards/lists_controller_spec.rb
index e5b8aa2e678..e1f75fa3395 100644
--- a/spec/controllers/boards/lists_controller_spec.rb
+++ b/spec/controllers/boards/lists_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::ListsController do
diff --git a/spec/controllers/concerns/checks_collaboration_spec.rb b/spec/controllers/concerns/checks_collaboration_spec.rb
index d7f110e11f3..7187e239486 100644
--- a/spec/controllers/concerns/checks_collaboration_spec.rb
+++ b/spec/controllers/concerns/checks_collaboration_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChecksCollaboration do
diff --git a/spec/controllers/concerns/continue_params_spec.rb b/spec/controllers/concerns/continue_params_spec.rb
index e2f683ae393..5e47f5e9f28 100644
--- a/spec/controllers/concerns/continue_params_spec.rb
+++ b/spec/controllers/concerns/continue_params_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ContinueParams do
diff --git a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
index 9b2d054f4fc..7a56f7203b0 100644
--- a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
+++ b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ControllerWithCrossProjectAccessCheck do
diff --git a/spec/controllers/concerns/enforces_admin_authentication_spec.rb b/spec/controllers/concerns/enforces_admin_authentication_spec.rb
new file mode 100644
index 00000000000..e6a6702fdea
--- /dev/null
+++ b/spec/controllers/concerns/enforces_admin_authentication_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EnforcesAdminAuthentication do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ controller(ApplicationController) do
+ # `described_class` is not available in this context
+ include EnforcesAdminAuthentication # rubocop:disable RSpec/DescribedClass
+
+ def index
+ head :ok
+ end
+ end
+
+ describe 'authenticate_admin!' do
+ context 'as an admin' do
+ let(:user) { create(:admin) }
+
+ it 'renders ok' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'as a user' do
+ it 'renders a 404' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/concerns/group_tree_spec.rb b/spec/controllers/concerns/group_tree_spec.rb
index 9fe17210d50..aa3cd690e3f 100644
--- a/spec/controllers/concerns/group_tree_spec.rb
+++ b/spec/controllers/concerns/group_tree_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupTree do
diff --git a/spec/controllers/concerns/import_url_params_spec.rb b/spec/controllers/concerns/import_url_params_spec.rb
new file mode 100644
index 00000000000..adbe6e5d3bf
--- /dev/null
+++ b/spec/controllers/concerns/import_url_params_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ImportUrlParams do
+ let(:import_url_params) do
+ controller = OpenStruct.new(params: params).extend(described_class)
+ controller.import_url_params
+ end
+
+ context 'empty URL' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ title: 'Test'
+ })
+ end
+
+ it 'returns empty hash' do
+ expect(import_url_params).to eq({})
+ end
+ end
+
+ context 'url and password separately provided' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ import_url: 'https://url.com',
+ import_url_user: 'user', import_url_password: 'password'
+ })
+ end
+
+ describe '#import_url_params' do
+ it 'returns hash with import_url' do
+ expect(import_url_params).to eq(
+ import_url: "https://user:password@url.com"
+ )
+ end
+ end
+ end
+
+ context 'url with provided empty credentials' do
+ let(:params) do
+ ActionController::Parameters.new(project: {
+ import_url: 'https://user:password@url.com',
+ import_url_user: '', import_url_password: ''
+ })
+ end
+
+ describe '#import_url_params' do
+ it 'does not change the url' do
+ expect(import_url_params).to eq(
+ import_url: "https://user:password@url.com"
+ )
+ end
+ end
+ end
+end
diff --git a/spec/controllers/concerns/internal_redirect_spec.rb b/spec/controllers/concerns/internal_redirect_spec.rb
index 7e23b56356e..97119438ca1 100644
--- a/spec/controllers/concerns/internal_redirect_spec.rb
+++ b/spec/controllers/concerns/internal_redirect_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe InternalRedirect do
diff --git a/spec/controllers/concerns/issuable_collections_spec.rb b/spec/controllers/concerns/issuable_collections_spec.rb
index 8580900215c..fb2cd5ca955 100644
--- a/spec/controllers/concerns/issuable_collections_spec.rb
+++ b/spec/controllers/concerns/issuable_collections_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssuableCollections do
@@ -106,51 +108,77 @@ describe IssuableCollections do
end
describe '#finder_options' do
- let(:params) do
- {
- assignee_id: '1',
- assignee_username: 'user1',
- author_id: '2',
- author_username: 'user2',
- authorized_only: 'yes',
- confidential: true,
- due_date: '2017-01-01',
- group_id: '3',
- iids: '4',
- label_name: 'foo',
- milestone_title: 'bar',
- my_reaction_emoji: 'thumbsup',
- non_archived: 'true',
- project_id: '5',
- scope: 'all',
- search: 'baz',
- sort: 'priority',
- state: 'opened',
- invalid_param: 'invalid_param'
- }
- end
-
- it 'only allows whitelisted params' do
+ before do
allow(controller).to receive(:cookies).and_return({})
allow(controller).to receive(:current_user).and_return(nil)
+ end
- finder_options = controller.send(:finder_options)
-
- expect(finder_options).to eq(ActionController::Parameters.new({
- 'assignee_id' => '1',
- 'assignee_username' => 'user1',
- 'author_id' => '2',
- 'author_username' => 'user2',
- 'confidential' => true,
- 'label_name' => 'foo',
- 'milestone_title' => 'bar',
- 'my_reaction_emoji' => 'thumbsup',
- 'due_date' => '2017-01-01',
- 'scope' => 'all',
- 'search' => 'baz',
- 'sort' => 'priority',
- 'state' => 'opened'
- }).permit!)
+ subject { controller.send(:finder_options).to_h }
+
+ context 'scalar params' do
+ let(:params) do
+ {
+ assignee_id: '1',
+ assignee_username: 'user1',
+ author_id: '2',
+ author_username: 'user2',
+ authorized_only: 'yes',
+ confidential: true,
+ due_date: '2017-01-01',
+ group_id: '3',
+ iids: '4',
+ label_name: 'foo',
+ milestone_title: 'bar',
+ my_reaction_emoji: 'thumbsup',
+ non_archived: 'true',
+ project_id: '5',
+ scope: 'all',
+ search: 'baz',
+ sort: 'priority',
+ state: 'opened',
+ invalid_param: 'invalid_param'
+ }
+ end
+
+ it 'only allows whitelisted params' do
+ is_expected.to include({
+ 'assignee_id' => '1',
+ 'assignee_username' => 'user1',
+ 'author_id' => '2',
+ 'author_username' => 'user2',
+ 'confidential' => true,
+ 'label_name' => 'foo',
+ 'milestone_title' => 'bar',
+ 'my_reaction_emoji' => 'thumbsup',
+ 'due_date' => '2017-01-01',
+ 'scope' => 'all',
+ 'search' => 'baz',
+ 'sort' => 'priority',
+ 'state' => 'opened'
+ })
+
+ is_expected.not_to include('invalid_param')
+ end
+ end
+
+ context 'array params' do
+ let(:params) do
+ {
+ assignee_username: %w[user1 user2],
+ label_name: %w[label1 label2],
+ invalid_param: 'invalid_param',
+ invalid_array: ['param']
+ }
+ end
+
+ it 'only allows whitelisted params' do
+ is_expected.to include({
+ 'label_name' => %w[label1 label2],
+ 'assignee_username' => %w[user1 user2]
+ })
+
+ is_expected.not_to include('invalid_param', 'invalid_array')
+ end
end
end
end
diff --git a/spec/controllers/concerns/lfs_request_spec.rb b/spec/controllers/concerns/lfs_request_spec.rb
index 7b49d4b6a3a..cb8c0b8f71c 100644
--- a/spec/controllers/concerns/lfs_request_spec.rb
+++ b/spec/controllers/concerns/lfs_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LfsRequest do
diff --git a/spec/controllers/concerns/project_unauthorized_spec.rb b/spec/controllers/concerns/project_unauthorized_spec.rb
new file mode 100644
index 00000000000..5834b1ef37f
--- /dev/null
+++ b/spec/controllers/concerns/project_unauthorized_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectUnauthorized do
+ include ExternalAuthorizationServiceHelpers
+ let(:user) { create(:user) }
+
+ before do
+ sign_in user
+ end
+
+ render_views
+
+ describe '.on_routable_not_found' do
+ controller(::Projects::ApplicationController) do
+ def show
+ head :ok
+ end
+ end
+
+ let(:project) { create(:project) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'renders a 200 when the service allows access to the project' do
+ external_service_allow_access(user, project)
+
+ get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'renders a 403 when the service denies access to the project' do
+ external_service_deny_access(user, project)
+
+ get :show, params: { namespace_id: project.namespace.to_param, id: project.to_param }
+
+ expect(response).to have_gitlab_http_status(403)
+ expect(response.body).to match("External authorization denied access to this project")
+ end
+
+ it 'renders a 404 when the user cannot see the project at all' do
+ other_project = create(:project, :private)
+
+ get :show, params: { namespace_id: other_project.namespace.to_param, id: other_project.to_param }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+end
diff --git a/spec/controllers/concerns/routable_actions_spec.rb b/spec/controllers/concerns/routable_actions_spec.rb
new file mode 100644
index 00000000000..59d48c68b9c
--- /dev/null
+++ b/spec/controllers/concerns/routable_actions_spec.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe RoutableActions do
+ controller(::ApplicationController) do
+ include RoutableActions # rubocop:disable RSpec/DescribedClass
+
+ before_action :routable
+
+ def routable
+ @klass = params[:type].constantize
+ @routable = find_routable!(params[:type].constantize, params[:id])
+ end
+
+ def show
+ head :ok
+ end
+ end
+
+ def get_routable(routable)
+ get :show, params: { id: routable.full_path, type: routable.class }
+ end
+
+ describe '#find_routable!' do
+ context 'when signed in' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'with a project' do
+ let(:routable) { create(:project) }
+
+ context 'when authorized' do
+ before do
+ routable.add_guest(user)
+ end
+
+ it 'returns the project' do
+ get_routable(routable)
+
+ expect(assigns[:routable]).to be_a(Project)
+ end
+
+ it 'allows access' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ it 'prevents access when not authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with a group' do
+ let(:routable) { create(:group, :private) }
+
+ context 'when authorized' do
+ before do
+ routable.add_guest(user)
+ end
+
+ it 'returns the group' do
+ get_routable(routable)
+
+ expect(assigns[:routable]).to be_a(Group)
+ end
+
+ it 'allows access' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ it 'prevents access when not authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with a user' do
+ let(:routable) { user }
+
+ it 'allows access when authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'prevents access when unauthorized' do
+ allow(subject).to receive(:can?).and_return(false)
+
+ get_routable(user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to sign in for private resouces' do
+ routable = create(:project, :private)
+
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.location).to end_with('/users/sign_in')
+ end
+ end
+ end
+
+ describe '#perform_not_found_actions' do
+ let(:routable) { create(:project) }
+
+ before do
+ sign_in(create(:user))
+ end
+
+ it 'performs multiple checks' do
+ last_check_called = false
+ checks = [proc {}, proc { last_check_called = true }]
+ allow(subject).to receive(:not_found_actions).and_return(checks)
+
+ get_routable(routable)
+
+ expect(last_check_called).to eq(true)
+ end
+
+ it 'performs checks in the context of the controller' do
+ check = lambda { |routable| redirect_to routable }
+ allow(subject).to receive(:not_found_actions).and_return([check])
+
+ get_routable(routable)
+
+ expect(response.location).to end_with(routable.to_param)
+ end
+
+ it 'skips checks once one has resulted in a render/redirect' do
+ first_check = proc { render plain: 'first' }
+ second_check = proc { render plain: 'second' }
+ allow(subject).to receive(:not_found_actions).and_return([first_check, second_check])
+
+ get_routable(routable)
+
+ expect(response.body).to eq('first')
+ end
+ end
+end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index cf3b24f50a3..a3ce08f736c 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -1,3 +1,6 @@
+# coding: utf-8
+# frozen_string_literal: true
+
require 'spec_helper'
describe SendFileUpload do
@@ -11,7 +14,7 @@ describe SendFileUpload do
# user/:id
def dynamic_segment
- File.join(model.class.to_s.underscore, model.id.to_s)
+ File.join(model.class.underscore, model.id.to_s)
end
end
end
@@ -112,7 +115,7 @@ describe SendFileUpload do
it 'sends a file with a custom type' do
headers = double
- expected_headers = %r(response-content-disposition=attachment%3B%20filename%3D%22test.js%22%3B%20filename%2A%3DUTF-8%27%27test.js&response-content-type=application/ecmascript)
+ expected_headers = /response-content-disposition=attachment%3B%20filename%3D%22test.js%22%3B%20filename%2A%3DUTF-8%27%27test.js&response-content-type=application%2Fecmascript/
expect(Gitlab::Workhorse).to receive(:send_url).with(expected_headers).and_call_original
expect(headers).to receive(:store).with(Gitlab::Workhorse::SEND_DATA_HEADER, /^send-url:/)
diff --git a/spec/controllers/dashboard/groups_controller_spec.rb b/spec/controllers/dashboard/groups_controller_spec.rb
index c8d99f79277..48373d29412 100644
--- a/spec/controllers/dashboard/groups_controller_spec.rb
+++ b/spec/controllers/dashboard/groups_controller_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Dashboard::GroupsController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
before do
@@ -11,33 +15,43 @@ describe Dashboard::GroupsController do
expect(described_class).to include(GroupTree)
end
- it 'only includes projects the user is a member of' do
- member_of_group = create(:group)
- member_of_group.add_developer(user)
- create(:group, :public)
+ describe '#index' do
+ it 'only includes projects the user is a member of' do
+ member_of_group = create(:group)
+ member_of_group.add_developer(user)
+ create(:group, :public)
- get :index
+ get :index
- expect(assigns(:groups)).to contain_exactly(member_of_group)
- end
+ expect(assigns(:groups)).to contain_exactly(member_of_group)
+ end
- context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do
- let!(:top_level_result) { create(:group, name: 'chef-top') }
- let!(:top_level_a) { create(:group, name: 'top-a') }
- let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) }
- let!(:other_group) { create(:group, name: 'other') }
+ context 'when rendering an expanded hierarchy with public groups you are not a member of', :nested_groups do
+ let!(:top_level_result) { create(:group, name: 'chef-top') }
+ let!(:top_level_a) { create(:group, name: 'top-a') }
+ let!(:sub_level_result_a) { create(:group, name: 'chef-sub-a', parent: top_level_a) }
+ let!(:other_group) { create(:group, name: 'other') }
- before do
- top_level_result.add_maintainer(user)
- top_level_a.add_maintainer(user)
+ before do
+ top_level_result.add_maintainer(user)
+ top_level_a.add_maintainer(user)
+ end
+
+ it 'renders only groups the user is a member of when searching hierarchy correctly' do
+ get :index, params: { filter: 'chef' }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ all_groups = [top_level_result, top_level_a, sub_level_result_a]
+ expect(assigns(:groups)).to contain_exactly(*all_groups)
+ end
end
- it 'renders only groups the user is a member of when searching hierarchy correctly' do
- get :index, params: { filter: 'chef' }, format: :json
+ it 'works when the external authorization service is enabled' do
+ enable_external_authorization_service_check
+
+ get :index
expect(response).to have_gitlab_http_status(200)
- all_groups = [top_level_result, top_level_a, sub_level_result_a]
- expect(assigns(:groups)).to contain_exactly(*all_groups)
end
end
end
diff --git a/spec/controllers/dashboard/labels_controller_spec.rb b/spec/controllers/dashboard/labels_controller_spec.rb
index a3bfb2f3a87..cb9c3660b9b 100644
--- a/spec/controllers/dashboard/labels_controller_spec.rb
+++ b/spec/controllers/dashboard/labels_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Dashboard::LabelsController do
@@ -13,13 +15,17 @@ describe Dashboard::LabelsController do
describe "#index" do
let!(:unrelated_label) { create(:label, project: create(:project, :public)) }
+ subject { get :index, format: :json }
+
it 'returns global labels for projects the user has a relationship with' do
- get :index, format: :json
+ subject
expect(json_response).to be_kind_of(Array)
expect(json_response.size).to eq(1)
expect(json_response[0]["id"]).to be_nil
expect(json_response[0]["title"]).to eq(label.title)
end
+
+ it_behaves_like 'disabled when using an external authorization service'
end
end
diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb
index 4b164d0aa6b..4de537ae6f8 100644
--- a/spec/controllers/dashboard/milestones_controller_spec.rb
+++ b/spec/controllers/dashboard/milestones_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Dashboard::MilestonesController do
@@ -13,7 +15,7 @@ describe Dashboard::MilestonesController do
)
end
let(:issue) { create(:issue, project: project, milestone: project_milestone) }
- let(:group_issue) { create(:issue, milestone: group_milestone) }
+ let(:group_issue) { create(:issue, milestone: group_milestone, project: create(:project, group: group)) }
let!(:label) { create(:label, project: project, title: 'Issue Label', issues: [issue]) }
let!(:group_label) { create(:group_label, group: group, title: 'Group Issue Label', issues: [group_issue]) }
@@ -75,11 +77,17 @@ describe Dashboard::MilestonesController do
expect(response.body).not_to include(project_milestone.title)
end
- it 'should show counts of group and project milestones to which the user belongs to' do
+ it 'shows counts of group and project milestones to which the user belongs to' do
get :index
expect(response.body).to include("Open\n<span class=\"badge badge-pill\">2</span>")
expect(response.body).to include("Closed\n<span class=\"badge badge-pill\">0</span>")
end
+
+ context 'external authorization' do
+ subject { get :index }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
end
diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb
index 2975205e09c..ea68eae12ed 100644
--- a/spec/controllers/dashboard/projects_controller_spec.rb
+++ b/spec/controllers/dashboard/projects_controller_spec.rb
@@ -1,5 +1,55 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Dashboard::ProjectsController do
- it_behaves_like 'authenticates sessionless user', :index, :atom
+ include ExternalAuthorizationServiceHelpers
+
+ describe '#index' do
+ context 'user not logged in' do
+ it_behaves_like 'authenticates sessionless user', :index, :atom
+ end
+
+ context 'user logged in' do
+ before do
+ sign_in create(:user)
+ end
+
+ context 'external authorization' do
+ it 'works when the external authorization service is enabled' do
+ enable_external_authorization_service_check
+
+ get :index
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
+ end
+
+ context 'json requests' do
+ render_views
+
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET /projects.json' do
+ before do
+ get :index, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
+ end
+
+ describe 'GET /starred.json' do
+ before do
+ get :starred, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
+ end
+ end
end
diff --git a/spec/controllers/dashboard/todos_controller_spec.rb b/spec/controllers/dashboard/todos_controller_spec.rb
index d88beaff0e1..6243ddc03c0 100644
--- a/spec/controllers/dashboard/todos_controller_spec.rb
+++ b/spec/controllers/dashboard/todos_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Dashboard::TodosController do
@@ -105,6 +107,12 @@ describe Dashboard::TodosController do
end
end
end
+
+ context 'external authorization' do
+ subject { get :index }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
describe 'PATCH #restore' do
diff --git a/spec/controllers/dashboard_controller_spec.rb b/spec/controllers/dashboard_controller_spec.rb
index c857a78d5e8..a733c3ecaa1 100644
--- a/spec/controllers/dashboard_controller_spec.rb
+++ b/spec/controllers/dashboard_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DashboardController do
@@ -23,4 +25,37 @@ describe DashboardController do
it_behaves_like 'authenticates sessionless user', :issues, :atom, author_id: User.first
it_behaves_like 'authenticates sessionless user', :issues_calendar, :ics
+
+ describe "#check_filters_presence!" do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ get :merge_requests, params: params
+ end
+
+ context "no filters" do
+ let(:params) { {} }
+
+ it 'sets @no_filters_set to false' do
+ expect(assigns[:no_filters_set]).to eq(true)
+ end
+ end
+
+ context "scalar filters" do
+ let(:params) { { author_id: user.id } }
+
+ it 'sets @no_filters_set to false' do
+ expect(assigns[:no_filters_set]).to eq(false)
+ end
+ end
+
+ context "array filters" do
+ let(:params) { { label_name: ['bug'] } }
+
+ it 'sets @no_filters_set to false' do
+ expect(assigns[:no_filters_set]).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/controllers/explore/groups_controller_spec.rb b/spec/controllers/explore/groups_controller_spec.rb
index 9e0ad9ea86f..5a32d8ca0d3 100644
--- a/spec/controllers/explore/groups_controller_spec.rb
+++ b/spec/controllers/explore/groups_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Explore::GroupsController do
diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb
index d57367e931e..463586ee422 100644
--- a/spec/controllers/explore/projects_controller_spec.rb
+++ b/spec/controllers/explore/projects_controller_spec.rb
@@ -1,6 +1,38 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Explore::ProjectsController do
+ describe 'GET #index.json' do
+ render_views
+
+ before do
+ get :index, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
+ end
+
+ describe 'GET #trending.json' do
+ render_views
+
+ before do
+ get :trending, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
+ end
+
+ describe 'GET #starred.json' do
+ render_views
+
+ before do
+ get :starred, format: :json
+ end
+
+ it { is_expected.to respond_with(:success) }
+ end
+
describe 'GET #trending' do
context 'sorting by update date' do
let(:project1) { create(:project, :public, updated_at: 3.days.ago) }
diff --git a/spec/controllers/google_api/authorizations_controller_spec.rb b/spec/controllers/google_api/authorizations_controller_spec.rb
index 1e8e82da4f3..940bf9c6828 100644
--- a/spec/controllers/google_api/authorizations_controller_spec.rb
+++ b/spec/controllers/google_api/authorizations_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GoogleApi::AuthorizationsController do
@@ -6,7 +8,7 @@ describe GoogleApi::AuthorizationsController do
let(:token) { 'token' }
let(:expires_at) { 1.hour.since.strftime('%s') }
- subject { get :callback, params: { code: 'xxx', state: @state } }
+ subject { get :callback, params: { code: 'xxx', state: state } }
before do
sign_in(user)
@@ -15,35 +17,57 @@ describe GoogleApi::AuthorizationsController do
.to receive(:get_token).and_return([token, expires_at])
end
- it 'sets token and expires_at in session' do
- subject
+ shared_examples_for 'access denied' do
+ it 'returns a 404' do
+ subject
- expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token])
- .to eq(token)
- expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at])
- .to eq(expires_at)
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil
+ expect(response).to have_http_status(:not_found)
+ end
end
- context 'when redirect uri key is stored in state' do
- set(:project) { create(:project) }
- let(:redirect_uri) { project_clusters_url(project).to_s }
+ context 'session key is present' do
+ let(:session_key) { 'session-key' }
+ let(:redirect_uri) { 'example.com' }
before do
- @state = GoogleApi::CloudPlatform::Client
- .new_session_key_for_redirect_uri do |key|
- session[key] = redirect_uri
+ session[GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(session_key)] = redirect_uri
+ end
+
+ context 'session key matches state param' do
+ let(:state) { session_key }
+
+ it 'sets token and expires_at in session' do
+ subject
+
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_token])
+ .to eq(token)
+ expect(session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at])
+ .to eq(expires_at)
+ end
+
+ it 'redirects to the URL stored in state param' do
+ expect(subject).to redirect_to(redirect_uri)
end
end
- it 'redirects to the URL stored in state param' do
- expect(subject).to redirect_to(redirect_uri)
+ context 'session key does not match state param' do
+ let(:state) { 'bad-key' }
+
+ it_behaves_like 'access denied'
end
- end
- context 'when redirection url is not stored in state' do
- it 'redirects to root_path' do
- expect(subject).to redirect_to(root_path)
+ context 'state param is blank' do
+ let(:state) { '' }
+
+ it_behaves_like 'access denied'
end
end
+
+ context 'state param is present, but session key is blank' do
+ let(:state) { 'session-key' }
+
+ it_behaves_like 'access denied'
+ end
end
end
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index a0f40874db1..c19a752b07b 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -1,112 +1,45 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GraphqlController do
- describe 'execute' do
- let(:user) { nil }
-
- before do
- sign_in(user) if user
-
- run_test_query!
- end
-
- subject { query_response }
+ before do
+ stub_feature_flags(graphql: true)
+ end
- context 'graphql is disabled by feature flag' do
- let(:user) { nil }
+ describe 'POST #execute' do
+ context 'when user is logged in' do
+ let(:user) { create(:user) }
before do
- stub_feature_flags(graphql: false)
- end
-
- it 'returns 404' do
- run_test_query!
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'signed out' do
- let(:user) { nil }
-
- it 'runs the query with current_user: nil' do
- is_expected.to eq('echo' => 'nil says: test success')
- end
- end
-
- context 'signed in' do
- let(:user) { create(:user, username: 'Simon') }
-
- it 'runs the query with current_user set' do
- is_expected.to eq('echo' => '"Simon" says: test success')
- end
- end
-
- context 'invalid variables' do
- it 'returns an error' do
- run_test_query!(variables: "This is not JSON")
-
- expect(response).to have_gitlab_http_status(422)
- expect(json_response['errors'].first['message']).not_to be_nil
+ sign_in(user)
end
- end
- end
-
- context 'token authentication' do
- before do
- stub_authentication_activity_metrics(debug: false)
- end
-
- let(:user) { create(:user, username: 'Simon') }
- let(:personal_access_token) { create(:personal_access_token, user: user) }
-
- context "when the 'personal_access_token' param is populated with the personal access token" do
- it 'logs the user in' do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
- .and increment(:user_session_override_counter)
- .and increment(:user_sessionless_authentication_counter)
- run_test_query!(private_token: personal_access_token.token)
+ it 'returns 200 when user can access API' do
+ post :execute
expect(response).to have_gitlab_http_status(200)
- expect(query_response).to eq('echo' => '"Simon" says: test success')
end
- end
-
- context 'when the personal access token has no api scope' do
- it 'does not log the user in' do
- personal_access_token.update(scopes: [:read_user])
- run_test_query!(private_token: personal_access_token.token)
+ it 'returns access denied template when user cannot access API' do
+ # User cannot access API in a couple of cases
+ # * When user is internal(like ghost users)
+ # * When user is blocked
+ expect(Ability).to receive(:allowed?).with(user, :access_api, :global).and_return(false)
- expect(response).to have_gitlab_http_status(200)
+ post :execute
- expect(query_response).to eq('echo' => 'nil says: test success')
+ expect(response.status).to eq(403)
+ expect(response).to render_template('errors/access_denied')
end
end
- context 'without token' do
- it 'shows public data' do
- run_test_query!
+ context 'when user is not logged in' do
+ it 'returns 200' do
+ post :execute
- expect(query_response).to eq('echo' => 'nil says: test success')
+ expect(response).to have_gitlab_http_status(200)
end
end
end
-
- # Chosen to exercise all the moving parts in GraphqlController#execute
- def run_test_query!(variables: { 'text' => 'test success' }, private_token: nil)
- query = <<~QUERY
- query Echo($text: String) {
- echo(text: $text)
- }
- QUERY
-
- post :execute, params: { query: query, operationName: 'Echo', variables: variables, private_token: private_token }
- end
-
- def query_response
- json_response['data']
- end
end
diff --git a/spec/controllers/groups/avatars_controller_spec.rb b/spec/controllers/groups/avatars_controller_spec.rb
index 772d1d0c1dd..7fffafaa2d4 100644
--- a/spec/controllers/groups/avatars_controller_spec.rb
+++ b/spec/controllers/groups/avatars_controller_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::AvatarsController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group, avatar: fixture_file_upload("spec/fixtures/dk.png", "image/png")) }
@@ -15,4 +19,12 @@ describe Groups::AvatarsController do
expect(@group.avatar.present?).to be_falsey
expect(@group).to be_valid
end
+
+ it 'works when external authorization service is enabled' do
+ enable_external_authorization_service_check
+
+ delete :destroy, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
end
diff --git a/spec/controllers/groups/boards_controller_spec.rb b/spec/controllers/groups/boards_controller_spec.rb
index 4228e727b52..881d0018b79 100644
--- a/spec/controllers/groups/boards_controller_spec.rb
+++ b/spec/controllers/groups/boards_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::BoardsController do
@@ -22,28 +24,6 @@ describe Groups::BoardsController do
expect(response.content_type).to eq 'text/html'
end
- it 'redirects to latest visited board' do
- board = create(:board, group: group)
- create(:board_group_recent_visit, group: board.group, board: board, user: user)
-
- list_boards
-
- expect(response).to redirect_to(group_board_path(id: board.id))
- end
-
- it 'renders template if visited board is not found' do
- temporary_board = create(:board, group: group)
- visited = create(:board_group_recent_visit, group: temporary_board.group, board: temporary_board, user: user)
- temporary_board.delete
-
- allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
-
- list_boards
-
- expect(response).to render_template :index
- expect(response.content_type).to eq 'text/html'
- end
-
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true)
@@ -104,6 +84,10 @@ describe Groups::BoardsController do
end
end
+ it_behaves_like 'disabled when using an external authorization service' do
+ subject { list_boards }
+ end
+
def list_boards(format: :html)
get :index, params: { group_id: group }, format: format
end
@@ -182,6 +166,10 @@ describe Groups::BoardsController do
end
end
+ it_behaves_like 'disabled when using an external authorization service' do
+ subject { read_board board: board }
+ end
+
def read_board(board:, format: :html)
get :show, params: {
group_id: group,
diff --git a/spec/controllers/groups/children_controller_spec.rb b/spec/controllers/groups/children_controller_spec.rb
index e1b97013408..02fb971bd9a 100644
--- a/spec/controllers/groups/children_controller_spec.rb
+++ b/spec/controllers/groups/children_controller_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::ChildrenController do
+ include ExternalAuthorizationServiceHelpers
+
let(:group) { create(:group, :public) }
let(:user) { create(:user) }
let!(:group_member) { create(:group_member, group: group, user: user) }
@@ -317,5 +321,15 @@ describe Groups::ChildrenController do
end
end
end
+
+ context 'external authorization' do
+ it 'works when external authorization service is enabled' do
+ enable_external_authorization_service_check
+
+ get :index, params: { group_id: group }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
end
diff --git a/spec/controllers/groups/clusters/applications_controller_spec.rb b/spec/controllers/groups/clusters/applications_controller_spec.rb
index dd5263b077c..16a63536ea6 100644
--- a/spec/controllers/groups/clusters/applications_controller_spec.rb
+++ b/spec/controllers/groups/clusters/applications_controller_spec.rb
@@ -9,9 +9,25 @@ describe Groups::Clusters::ApplicationsController do
Clusters::Cluster::APPLICATIONS[application]
end
+ shared_examples 'a secure endpoint' do
+ it { expect { subject }.to be_allowed_for(:admin) }
+ it { expect { subject }.to be_allowed_for(:owner).of(group) }
+ it { expect { subject }.to be_allowed_for(:maintainer).of(group) }
+ it { expect { subject }.to be_denied_for(:developer).of(group) }
+ it { expect { subject }.to be_denied_for(:reporter).of(group) }
+ it { expect { subject }.to be_denied_for(:guest).of(group) }
+ it { expect { subject }.to be_denied_for(:user) }
+ it { expect { subject }.to be_denied_for(:external) }
+ end
+
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
+
describe 'POST create' do
- let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
- let(:group) { cluster.group }
+ subject do
+ post :create, params: params.merge(group_id: group)
+ end
+
let(:application) { 'helm' }
let(:params) { { application: application, id: cluster.id } }
@@ -26,7 +42,7 @@ describe Groups::Clusters::ApplicationsController do
it 'schedule an application installation' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
- expect { go }.to change { current_application.count }
+ expect { subject }.to change { current_application.count }
expect(response).to have_http_status(:no_content)
expect(cluster.application_helm).to be_scheduled
end
@@ -37,7 +53,7 @@ describe Groups::Clusters::ApplicationsController do
end
it 'return 404' do
- expect { go }.not_to change { current_application.count }
+ expect { subject }.not_to change { current_application.count }
expect(response).to have_http_status(:not_found)
end
end
@@ -46,9 +62,7 @@ describe Groups::Clusters::ApplicationsController do
let(:application) { 'unkwnown-app' }
it 'return 404' do
- go
-
- expect(response).to have_http_status(:not_found)
+ is_expected.to have_http_status(:not_found)
end
end
@@ -58,9 +72,7 @@ describe Groups::Clusters::ApplicationsController do
end
it 'returns 400' do
- go
-
- expect(response).to have_http_status(:bad_request)
+ is_expected.to have_http_status(:bad_request)
end
end
end
@@ -70,18 +82,66 @@ describe Groups::Clusters::ApplicationsController do
allow(ClusterInstallAppWorker).to receive(:perform_async)
end
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(group) }
- it { expect { go }.to be_allowed_for(:maintainer).of(group) }
- it { expect { go }.to be_denied_for(:developer).of(group) }
- it { expect { go }.to be_denied_for(:reporter).of(group) }
- it { expect { go }.to be_denied_for(:guest).of(group) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
+ it_behaves_like 'a secure endpoint'
end
+ end
- def go
- post :create, params: params.merge(group_id: group)
+ describe 'PATCH update' do
+ subject do
+ patch :update, params: params.merge(group_id: group)
+ end
+
+ let!(:application) { create(:clusters_applications_cert_managers, :installed, cluster: cluster) }
+ let(:application_name) { application.name }
+ let(:params) { { application: application_name, id: cluster.id, email: "new-email@example.com" } }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ group.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context "when cluster and app exists" do
+ it "schedules an application update" do
+ expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once
+
+ is_expected.to have_http_status(:no_content)
+
+ expect(cluster.application_cert_manager).to be_scheduled
+ end
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is unknown' do
+ let(:application_name) { 'unkwnown-app' }
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is already scheduled' do
+ before do
+ application.make_scheduled!
+ end
+
+ it { is_expected.to have_http_status(:bad_request) }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterPatchAppWorker).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
end
end
end
diff --git a/spec/controllers/groups/clusters_controller_spec.rb b/spec/controllers/groups/clusters_controller_spec.rb
index ef23ffaa843..7349cb7094c 100644
--- a/spec/controllers/groups/clusters_controller_spec.rb
+++ b/spec/controllers/groups/clusters_controller_spec.rb
@@ -189,6 +189,7 @@ describe Groups::ClustersController do
{
cluster: {
name: 'new-cluster',
+ managed: '1',
provider_gcp_attributes: {
gcp_project_id: 'gcp-project-12345',
legacy_abac: legacy_abac_param
@@ -218,6 +219,7 @@ describe Groups::ClustersController do
expect(cluster).to be_gcp
expect(cluster).to be_kubernetes
expect(cluster.provider_gcp).to be_legacy_abac
+ expect(cluster).to be_managed
end
context 'when legacy_abac param is false' do
@@ -278,6 +280,7 @@ describe Groups::ClustersController do
{
cluster: {
name: 'new-cluster',
+ managed: '1',
platform_kubernetes_attributes: {
api_url: 'http://my-url',
token: 'test'
@@ -303,6 +306,7 @@ describe Groups::ClustersController do
expect(response).to redirect_to(group_cluster_path(group, cluster))
expect(cluster).to be_user
expect(cluster).to be_kubernetes
+ expect(cluster).to be_managed
end
end
@@ -334,6 +338,29 @@ describe Groups::ClustersController do
expect(cluster).to be_platform_kubernetes_rbac
end
end
+
+ context 'when creates a user-managed cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ managed: '0',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ authorization_type: 'rbac'
+ }
+ }
+ }
+ end
+
+ it 'creates a new user-managed cluster' do
+ go
+
+ cluster = group.clusters.first
+ expect(cluster.managed?).to be_falsy
+ end
+ end
end
describe 'security' do
@@ -455,7 +482,7 @@ describe Groups::ClustersController do
context 'when domain is invalid' do
let(:domain) { 'http://not-a-valid-domain' }
- it 'should not update cluster attributes' do
+ it 'does not update cluster attributes' do
go
cluster.reload
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 3a801fabafc..413598ddde0 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -1,8 +1,13 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::GroupMembersController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
let(:group) { create(:group, :public, :access_requestable) }
+ let(:membership) { create(:group_member, group: group) }
describe 'GET index' do
it 'renders index with 200 status code' do
@@ -263,4 +268,87 @@ describe Groups::GroupMembersController do
end
end
end
+
+ context 'with external authorization enabled' do
+ before do
+ enable_external_authorization_service_check
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'is successful' do
+ get :index, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'POST #create' do
+ it 'is successful' do
+ post :create, params: { group_id: group, users: user, access_level: Gitlab::Access::GUEST }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'PUT #update' do
+ it 'is successful' do
+ put :update,
+ params: {
+ group_member: { access_level: Gitlab::Access::GUEST },
+ group_id: group,
+ id: membership
+ },
+ format: :js
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ it 'is successful' do
+ delete :destroy, params: { group_id: group, id: membership }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'POST #destroy' do
+ it 'is successful' do
+ sign_in(create(:user))
+
+ post :request_access, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'POST #approve_request_access' do
+ it 'is successful' do
+ access_request = create(:group_member, :access_request, group: group)
+ post :approve_access_request, params: { group_id: group, id: access_request }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'DELETE #leave' do
+ it 'is successful' do
+ group.add_owner(create(:user))
+
+ delete :leave, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'POST #resend_invite' do
+ it 'is successful' do
+ post :resend_invite, params: { group_id: group, id: membership }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
index fa664a29066..3cc6fc6f066 100644
--- a/spec/controllers/groups/labels_controller_spec.rb
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::LabelsController do
@@ -37,6 +39,12 @@ describe Groups::LabelsController do
expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title])
end
end
+
+ context 'external authorization' do
+ subject { get :index, params: { group_id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
describe 'POST #toggle_subscription' do
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
index 043cf28514b..19b18091aef 100644
--- a/spec/controllers/groups/milestones_controller_spec.rb
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::MilestonesController do
@@ -80,6 +82,12 @@ describe Groups::MilestonesController do
expect(response.content_type).to eq 'application/json'
end
end
+
+ context 'external authorization' do
+ subject { get :index, params: { group_id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
describe '#show' do
diff --git a/spec/controllers/groups/runners_controller_spec.rb b/spec/controllers/groups/runners_controller_spec.rb
index 469459bfc02..91f9e2c7832 100644
--- a/spec/controllers/groups/runners_controller_spec.rb
+++ b/spec/controllers/groups/runners_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::RunnersController do
diff --git a/spec/controllers/groups/settings/ci_cd_controller_spec.rb b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
index 40673d10b91..70b3a5fb496 100644
--- a/spec/controllers/groups/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/groups/settings/ci_cd_controller_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::Settings::CiCdController do
+ include ExternalAuthorizationServiceHelpers
+
let(:group) { create(:group) }
let(:user) { create(:user) }
@@ -33,6 +37,19 @@ describe Groups::Settings::CiCdController do
expect(response).to have_gitlab_http_status(404)
end
end
+
+ context 'external authorization' do
+ before do
+ enable_external_authorization_service_check
+ group.add_owner(user)
+ end
+
+ it 'renders show with 200 status code' do
+ get :show, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
end
describe 'PUT #reset_registration_token' do
@@ -66,4 +83,77 @@ describe Groups::Settings::CiCdController do
end
end
end
+
+ describe 'PATCH #update_auto_devops' do
+ let(:auto_devops_param) { '1' }
+
+ subject do
+ patch :update_auto_devops, params: {
+ group_id: group,
+ group: { auto_devops_enabled: auto_devops_param }
+ }
+ end
+
+ context 'when user does not have enough permission' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { is_expected.to have_gitlab_http_status(404) }
+ end
+
+ context 'when user has enough privileges' do
+ before do
+ group.add_owner(user)
+ end
+
+ it { is_expected.to redirect_to(group_settings_ci_cd_path) }
+
+ context 'when service execution went wrong' do
+ before do
+ allow_any_instance_of(Groups::AutoDevopsService).to receive(:execute).and_return(false)
+ allow_any_instance_of(Group).to receive_message_chain(:errors, :full_messages)
+ .and_return(['Error 1'])
+
+ subject
+ end
+
+ it 'returns a flash alert' do
+ expect(response).to set_flash[:alert]
+ .to eq("There was a problem updating Auto DevOps pipeline: [\"Error 1\"].")
+ end
+ end
+
+ context 'when service execution was successful' do
+ it 'returns a flash notice' do
+ subject
+
+ expect(response).to set_flash[:notice]
+ .to eq('Auto DevOps pipeline was updated for the group')
+ end
+ end
+
+ context 'when changing auto devops value' do
+ before do
+ subject
+
+ group.reload
+ end
+
+ context 'when explicitly enabling auto devops' do
+ it 'updates group attribute' do
+ expect(group.auto_devops_enabled).to eq(true)
+ end
+ end
+
+ context 'when explicitly disabling auto devops' do
+ let(:auto_devops_param) { '0' }
+
+ it 'updates group attribute' do
+ expect(group.auto_devops_enabled).to eq(false)
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups/shared_projects_controller_spec.rb b/spec/controllers/groups/shared_projects_controller_spec.rb
index dab7700cf64..9f6c558c931 100644
--- a/spec/controllers/groups/shared_projects_controller_spec.rb
+++ b/spec/controllers/groups/shared_projects_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::SharedProjectsController do
@@ -6,6 +8,8 @@ describe Groups::SharedProjectsController do
end
def share_project(project)
+ group.add_developer(user)
+
Projects::GroupLinks::CreateService.new(
project,
user,
diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb
index 0104ba827da..0f99a957581 100644
--- a/spec/controllers/groups/uploads_controller_spec.rb
+++ b/spec/controllers/groups/uploads_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::UploadsController do
diff --git a/spec/controllers/groups/variables_controller_spec.rb b/spec/controllers/groups/variables_controller_spec.rb
index 29ec3588316..2d9c5c9d799 100644
--- a/spec/controllers/groups/variables_controller_spec.rb
+++ b/spec/controllers/groups/variables_controller_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::VariablesController do
+ include ExternalAuthorizationServiceHelpers
+
let(:group) { create(:group) }
let(:user) { create(:user) }
@@ -34,4 +38,36 @@ describe Groups::VariablesController do
include_examples 'PATCH #update updates variables'
end
+
+ context 'with external authorization enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ describe 'GET #show' do
+ let!(:variable) { create(:ci_group_variable, group: group) }
+
+ it 'is successful' do
+ get :show, params: { group_id: group }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'PATCH #update' do
+ let!(:variable) { create(:ci_group_variable, group: group) }
+ let(:owner) { group }
+
+ it 'is successful' do
+ patch :update,
+ params: {
+ group_id: group,
+ variables_attributes: [{ id: variable.id, key: 'hello' }]
+ },
+ format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 7d87b33e503..47d7e278183 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupsController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:group) { create(:group, :public) }
@@ -32,21 +36,46 @@ describe GroupsController do
end
end
+ shared_examples 'details view' do
+ it { is_expected.to render_template('groups/show') }
+
+ context 'as atom' do
+ let!(:event) { create(:event, project: project) }
+ let(:format) { :atom }
+
+ it { is_expected.to render_template('groups/show') }
+
+ it 'assigns events for all the projects in the group' do
+ subject
+ expect(assigns(:events)).to contain_exactly(event)
+ end
+ end
+ end
+
describe 'GET #show' do
before do
sign_in(user)
project
end
- context 'as atom' do
- it 'assigns events for all the projects in the group' do
- create(:event, project: project)
+ let(:format) { :html }
- get :show, params: { id: group.to_param }, format: :atom
+ subject { get :show, params: { id: group.to_param }, format: format }
- expect(assigns(:events)).not_to be_empty
- end
+ it_behaves_like 'details view'
+ end
+
+ describe 'GET #details' do
+ before do
+ sign_in(user)
+ project
end
+
+ let(:format) { :html }
+
+ subject { get :details, params: { id: group.to_param }, format: format }
+
+ it_behaves_like 'details view'
end
describe 'GET edit' do
@@ -112,6 +141,28 @@ describe GroupsController do
end
describe 'POST #create' do
+ it 'allows creating a group' do
+ sign_in(user)
+
+ expect do
+ post :create, params: { group: { name: 'new_group', path: "new_group" } }
+ end.to change { Group.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+
+ context 'authorization' do
+ it 'allows an admin to create a group' do
+ sign_in(create(:admin))
+
+ expect do
+ post :create, params: { group: { name: 'new_group', path: "new_group" } }
+ end.to change { Group.count }.by(1)
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
context 'when creating subgroups', :nested_groups do
[true, false].each do |can_create_group_status|
context "and can_create_group is #{can_create_group_status}" do
@@ -227,9 +278,7 @@ describe GroupsController do
context 'searching' do
before do
- # Remove in https://gitlab.com/gitlab-org/gitlab-ce/issues/54643
- stub_feature_flags(use_cte_for_group_issues_search: false)
- stub_feature_flags(use_subquery_for_group_issues_search: true)
+ stub_feature_flags(attempt_group_search_optimizations: true)
end
it 'works with popularity sort' do
@@ -326,6 +375,13 @@ describe GroupsController do
expect(assigns(:group).errors).not_to be_empty
expect(assigns(:group).path).not_to eq('new_path')
end
+
+ it 'updates the project_creation_level successfully' do
+ post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS } }
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(group.reload.project_creation_level).to eq(::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ end
end
describe '#ensure_canonical_path' do
@@ -543,11 +599,11 @@ describe GroupsController do
}
end
- it 'should return a notice' do
+ it 'returns a notice' do
expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
end
- it 'should redirect to the new path' do
+ it 'redirects to the new path' do
expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}")
end
end
@@ -564,11 +620,11 @@ describe GroupsController do
}
end
- it 'should return a notice' do
+ it 'returns a notice' do
expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
end
- it 'should redirect to the new path' do
+ it 'redirects to the new path' do
expect(response).to redirect_to("/#{group.path}")
end
end
@@ -588,12 +644,12 @@ describe GroupsController do
}
end
- it 'should return an alert' do
+ it 'returns an alert' do
expect(flash[:alert]).to eq "Transfer failed: namespace directory cannot be moved"
end
- it 'should redirect to the current path' do
- expect(response).to render_template(:edit)
+ it 'redirects to the current path' do
+ expect(response).to redirect_to(edit_group_path(group))
end
end
@@ -610,7 +666,7 @@ describe GroupsController do
}
end
- it 'should be denied' do
+ it 'is denied' do
expect(response).to have_gitlab_http_status(404)
end
end
@@ -635,4 +691,98 @@ describe GroupsController do
end
end
end
+
+ describe 'external authorization' do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ context 'with external authorization service enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ describe 'GET #show' do
+ it 'is successful' do
+ get :show, params: { id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'does not allow other formats' do
+ get :show, params: { id: group.to_param }, format: :atom
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ describe 'GET #edit' do
+ it 'is successful' do
+ get :edit, params: { id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'GET #new' do
+ it 'is successful' do
+ get :new
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'GET #index' do
+ it 'is successful' do
+ get :index
+
+ # Redirects to the dashboard
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+
+ describe 'POST #create' do
+ it 'creates a group' do
+ expect do
+ post :create, params: { group: { name: 'a name', path: 'a-name' } }
+ end.to change { Group.count }.by(1)
+ end
+ end
+
+ describe 'PUT #update' do
+ it 'updates a group' do
+ expect do
+ put :update, params: { id: group.to_param, group: { name: 'world' } }
+ end.to change { group.reload.name }
+ end
+ end
+
+ describe 'DELETE #destroy' do
+ it 'deletes the group' do
+ delete :destroy, params: { id: group.to_param }
+
+ expect(response).to have_gitlab_http_status(302)
+ end
+ end
+ end
+
+ describe 'GET #activity' do
+ subject { get :activity, params: { id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
+
+ describe 'GET #issues' do
+ subject { get :issues, params: { id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
+
+ describe 'GET #merge_requests' do
+ subject { get :merge_requests, params: { id: group.to_param } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
+ end
end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index 29e159ad5d7..19d739fcf4f 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe HealthCheckController do
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index f685f2b41c0..fc62a8310aa 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe HealthController do
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index dca67c18caa..dbfacf4e42e 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe HelpController do
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 0bc09c86939..64a66502732 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Import::BitbucketController do
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index a125e6ed16d..b89d7317b9c 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Import::BitbucketServerController do
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
index 5f0f6dea821..f1e0923f316 100644
--- a/spec/controllers/import/fogbugz_controller_spec.rb
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Import::FogbugzController do
diff --git a/spec/controllers/import/gitea_controller_spec.rb b/spec/controllers/import/gitea_controller_spec.rb
index 8cbec79095f..b7bdfcc3dc6 100644
--- a/spec/controllers/import/gitea_controller_spec.rb
+++ b/spec/controllers/import/gitea_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Import::GiteaController do
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 162dff98ec5..059354870b5 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Import::GithubController do
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index a874a7d36f6..5af7572e74e 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Import::GitlabController do
diff --git a/spec/controllers/import/gitlab_projects_controller_spec.rb b/spec/controllers/import/gitlab_projects_controller_spec.rb
index 55bd8ae7182..51b398895bc 100644
--- a/spec/controllers/import/gitlab_projects_controller_spec.rb
+++ b/spec/controllers/import/gitlab_projects_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Import::GitlabProjectsController do
diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb
index 3e5ed2afd93..17be91c0bbb 100644
--- a/spec/controllers/import/google_code_controller_spec.rb
+++ b/spec/controllers/import/google_code_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Import::GoogleCodeController do
diff --git a/spec/controllers/import/phabricator_controller_spec.rb b/spec/controllers/import/phabricator_controller_spec.rb
new file mode 100644
index 00000000000..85085a8e996
--- /dev/null
+++ b/spec/controllers/import/phabricator_controller_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Import::PhabricatorController do
+ let(:current_user) { create(:user) }
+
+ before do
+ sign_in current_user
+ end
+
+ describe 'GET #new' do
+ subject { get :new }
+
+ context 'when the import source is not available' do
+ before do
+ stub_feature_flags(phabricator_import: true)
+ stub_application_setting(import_sources: [])
+ end
+
+ it { is_expected.to have_gitlab_http_status(404) }
+ end
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(phabricator_import: false)
+ stub_application_setting(import_sources: ['phabricator'])
+ end
+
+ it { is_expected.to have_gitlab_http_status(404) }
+ end
+
+ context 'when the import is available' do
+ before do
+ stub_feature_flags(phabricator_import: true)
+ stub_application_setting(import_sources: ['phabricator'])
+ end
+
+ it { is_expected.to have_gitlab_http_status(200) }
+ end
+ end
+
+ describe 'POST #create' do
+ subject(:post_create) { post :create, params: params }
+
+ context 'with valid params' do
+ let(:params) do
+ { path: 'phab-import',
+ name: 'Phab import',
+ phabricator_server_url: 'https://phabricator.example.com',
+ api_token: 'hazaah',
+ namespace_id: current_user.namespace_id }
+ end
+
+ it 'creates a project to import' do
+ expect_next_instance_of(Gitlab::PhabricatorImport::Importer) do |importer|
+ expect(importer).to receive(:execute)
+ end
+
+ expect { post_create }.to change { current_user.namespace.projects.reload.size }.from(0).to(1)
+
+ expect(current_user.namespace.projects.last).to be_import
+ end
+ end
+
+ context 'when an import param is missing' do
+ let(:params) do
+ { path: 'phab-import',
+ name: 'Phab import',
+ phabricator_server_url: nil,
+ api_token: 'hazaah',
+ namespace_id: current_user.namespace_id }
+ end
+
+ it 'does not create the project' do
+ expect { post_create }.not_to change { current_user.namespace.projects.reload.size }
+ end
+ end
+
+ context 'when a project param is missing' do
+ let(:params) do
+ { phabricator_server_url: 'https://phabricator.example.com',
+ api_token: 'hazaah',
+ namespace_id: current_user.namespace_id }
+ end
+
+ it 'does not create the project' do
+ expect { post_create }.not_to change { current_user.namespace.projects.reload.size }
+ end
+ end
+ end
+end
diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb
index 7bbaf36e4df..ac0adcd06a3 100644
--- a/spec/controllers/invites_controller_spec.rb
+++ b/spec/controllers/invites_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe InvitesController do
diff --git a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
index c9d36a16008..6d588c8f915 100644
--- a/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ldap::OmniauthCallbacksController do
@@ -43,7 +45,7 @@ describe Ldap::OmniauthCallbacksController do
end
context 'sign up' do
- let(:user) { double(email: 'new@example.com') }
+ let(:user) { double(email: +'new@example.com') }
before do
stub_omniauth_setting(block_auto_created_users: false)
diff --git a/spec/controllers/metrics_controller_spec.rb b/spec/controllers/metrics_controller_spec.rb
index c7c83369d7c..ee454a7818c 100644
--- a/spec/controllers/metrics_controller_spec.rb
+++ b/spec/controllers/metrics_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MetricsController do
diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb
index cf52ce834b6..46328148eff 100644
--- a/spec/controllers/notification_settings_controller_spec.rb
+++ b/spec/controllers/notification_settings_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NotificationSettingsController do
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index caf2b87428b..228c97d591d 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Oauth::ApplicationsController do
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index cc8fa2c01b4..41f7684051e 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Oauth::AuthorizationsController do
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index 232a5e2793b..6e374a8daa7 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe OmniauthCallbacksController, type: :controller do
@@ -7,10 +9,14 @@ describe OmniauthCallbacksController, type: :controller do
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
before do
- mock_auth_hash(provider.to_s, extern_uid, user.email)
+ @original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, +extern_uid, user.email)
stub_omniauth_provider(provider, context: request)
end
+ after do
+ Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth
+ end
+
context 'when the user is on the last sign in attempt' do
let(:extern_uid) { 'my-uid' }
@@ -55,7 +61,7 @@ describe OmniauthCallbacksController, type: :controller do
allow(@routes).to receive(:generate_extras) { [path, []] }
end
- it 'it calls through to the failure handler' do
+ it 'calls through to the failure handler' do
request.env['omniauth.error'] = OneLogin::RubySaml::ValidationError.new("Fingerprint mismatch")
request.env['omniauth.error.strategy'] = OmniAuth::Strategies::SAML.new(nil)
stub_route_as('/users/auth/saml/callback')
@@ -113,8 +119,35 @@ describe OmniauthCallbacksController, type: :controller do
expect(request.env['warden']).to be_authenticated
end
+ context 'when user has no linked provider' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in user
+ end
+
+ it 'links identity' do
+ expect do
+ post provider
+ user.reload
+ end.to change { user.identities.count }.by(1)
+ end
+
+ context 'and is not allowed to link the provider' do
+ before do
+ allow_any_instance_of(IdentityProviderPolicy).to receive(:can?).with(:link).and_return(false)
+ end
+
+ it 'returns 403' do
+ post provider
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
shared_context 'sign_up' do
- let(:user) { double(email: 'new@example.com') }
+ let(:user) { double(email: +'new@example.com') }
before do
stub_omniauth_setting(block_auto_created_users: false)
@@ -193,7 +226,7 @@ describe OmniauthCallbacksController, type: :controller do
before do
stub_omniauth_saml_config({ enabled: true, auto_link_saml_user: true, allow_single_sign_on: ['saml'],
providers: [saml_config] })
- mock_auth_hash('saml', 'my-uid', user.email, mock_saml_response)
+ mock_auth_hash_with_saml_xml('saml', +'my-uid', user.email, mock_saml_response)
request.env["devise.mapping"] = Devise.mappings[:user]
request.env['omniauth.auth'] = Rails.application.env_config['omniauth.auth']
post :saml, params: { SAMLResponse: mock_saml_response }
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index 0af55cf3408..f2db024804d 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PasswordsController do
diff --git a/spec/controllers/profiles/accounts_controller_spec.rb b/spec/controllers/profiles/accounts_controller_spec.rb
index bb2ab27e2dd..f481b5078f2 100644
--- a/spec/controllers/profiles/accounts_controller_spec.rb
+++ b/spec/controllers/profiles/accounts_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Profiles::AccountsController do
diff --git a/spec/controllers/profiles/avatars_controller_spec.rb b/spec/controllers/profiles/avatars_controller_spec.rb
index 1ee0bf44e92..1a64cb72265 100644
--- a/spec/controllers/profiles/avatars_controller_spec.rb
+++ b/spec/controllers/profiles/avatars_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Profiles::AvatarsController do
diff --git a/spec/controllers/profiles/emails_controller_spec.rb b/spec/controllers/profiles/emails_controller_spec.rb
index a8a1f96befe..7c6b1863202 100644
--- a/spec/controllers/profiles/emails_controller_spec.rb
+++ b/spec/controllers/profiles/emails_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Profiles::EmailsController do
diff --git a/spec/controllers/profiles/keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb
index 5e2cc82bd8c..753eb432c5e 100644
--- a/spec/controllers/profiles/keys_controller_spec.rb
+++ b/spec/controllers/profiles/keys_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Profiles::KeysController do
diff --git a/spec/controllers/profiles/notifications_controller_spec.rb b/spec/controllers/profiles/notifications_controller_spec.rb
index 1b76446a0cf..f69847119d4 100644
--- a/spec/controllers/profiles/notifications_controller_spec.rb
+++ b/spec/controllers/profiles/notifications_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Profiles::NotificationsController do
diff --git a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
index 021bf2429e3..b467ecc4af9 100644
--- a/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
+++ b/spec/controllers/profiles/personal_access_tokens_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Profiles::PersonalAccessTokensController do
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index ee881f85233..929d57ebaec 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Profiles::PreferencesController do
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index 0151a434998..bcda8573468 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Profiles::TwoFactorAuthsController do
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 11cb59aa12a..08681c0341a 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require('spec_helper')
describe ProfilesController, :request_store do
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 29df00e6bb0..6ea82785e98 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ArtifactsController do
diff --git a/spec/controllers/projects/autocomplete_sources_controller_spec.rb b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
index 4bc72042710..a9a058e7e17 100644
--- a/spec/controllers/projects/autocomplete_sources_controller_spec.rb
+++ b/spec/controllers/projects/autocomplete_sources_controller_spec.rb
@@ -35,4 +35,35 @@ describe Projects::AutocompleteSourcesController do
avatar_url: user.avatar_url)
end
end
+
+ describe 'GET milestones' do
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, namespace: group) }
+ let!(:project_milestone) { create(:milestone, project: project) }
+ let!(:group_milestone) { create(:milestone, group: group) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'lists milestones' do
+ group.add_owner(user)
+
+ get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ milestone_titles = json_response.map { |milestone| milestone["title"] }
+ expect(milestone_titles).to match_array([project_milestone.title, group_milestone.title])
+ end
+
+ context 'when user cannot read project issues and merge requests' do
+ it 'renders 404' do
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+
+ get :milestones, format: :json, params: { namespace_id: group.path, project_id: project.path }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/avatars_controller_spec.rb b/spec/controllers/projects/avatars_controller_spec.rb
index 95b7ae5885a..d463619ad0b 100644
--- a/spec/controllers/projects/avatars_controller_spec.rb
+++ b/spec/controllers/projects/avatars_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::AvatarsController do
@@ -37,7 +39,7 @@ describe Projects::AvatarsController do
end
context 'when the avatar is stored in lfs' do
- it_behaves_like 'repository lfs file load' do
+ it_behaves_like 'a controller that can serve LFS files' do
let(:filename) { 'lfs_object.iso' }
let(:filepath) { "files/lfs/#{filename}" }
end
diff --git a/spec/controllers/projects/badges_controller_spec.rb b/spec/controllers/projects/badges_controller_spec.rb
index 8eac3d9a459..5ec8d8d41d7 100644
--- a/spec/controllers/projects/badges_controller_spec.rb
+++ b/spec/controllers/projects/badges_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::BadgesController do
diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb
index eb110ea0002..f901fd45604 100644
--- a/spec/controllers/projects/blame_controller_spec.rb
+++ b/spec/controllers/projects/blame_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::BlameController do
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 38957e96798..44500d3cde3 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Projects::BlobController do
@@ -10,6 +12,8 @@ describe Projects::BlobController do
context 'with file path' do
before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
get(:show,
params: {
namespace_id: project.namespace,
@@ -144,54 +148,34 @@ describe Projects::BlobController do
end
context 'when rendering for merge request' do
- it 'renders diff context lines Gitlab::Diff::Line array' do
- do_get(since: 1, to: 5, offset: 10, from_merge_request: true)
-
- lines = JSON.parse(response.body)
-
- expect(lines.first).to have_key('type')
- expect(lines.first).to have_key('rich_text')
- expect(lines.first).to have_key('rich_text')
+ let(:presenter) { double(:presenter, diff_lines: diff_lines) }
+ let(:diff_lines) do
+ Array.new(3, Gitlab::Diff::Line.new('plain', nil, nil, nil, nil, rich_text: 'rich'))
end
- context 'when rendering match lines' do
- it 'adds top match line when "since" is less than 1' do
- do_get(since: 5, to: 10, offset: 10, from_merge_request: true)
-
- match_line = JSON.parse(response.body).first
-
- expect(match_line['type']).to eq('match')
- expect(match_line['meta_data']).to have_key('old_pos')
- expect(match_line['meta_data']).to have_key('new_pos')
- end
-
- it 'does not add top match line when "since" is equal 1' do
- do_get(since: 1, to: 10, offset: 10, from_merge_request: true)
-
- match_line = JSON.parse(response.body).first
-
- expect(match_line['type']).to be_nil
- end
+ before do
+ allow(Blobs::UnfoldPresenter).to receive(:new).and_return(presenter)
+ end
- it 'adds bottom match line when "t"o is less than blob size' do
- do_get(since: 1, to: 5, offset: 10, from_merge_request: true, bottom: true)
+ it 'renders diff context lines Gitlab::Diff::Line array' do
+ do_get(since: 1, to: 2, offset: 0, from_merge_request: true)
- match_line = JSON.parse(response.body).last
+ lines = JSON.parse(response.body)
- expect(match_line['type']).to eq('match')
- expect(match_line['meta_data']).to have_key('old_pos')
- expect(match_line['meta_data']).to have_key('new_pos')
+ expect(lines.size).to eq(diff_lines.size)
+ lines.each do |line|
+ expect(line).to have_key('type')
+ expect(line['text']).to eq('plain')
+ expect(line['rich_text']).to eq('rich')
end
+ end
- it 'does not add bottom match line when "to" is less than blob size' do
- commit_id = project.repository.commit('master').id
- blob = project.repository.blob_at(commit_id, 'CHANGELOG')
- do_get(since: 1, to: blob.lines.count, offset: 10, from_merge_request: true, bottom: true)
+ it 'handles full being true' do
+ do_get(full: true, from_merge_request: true)
- match_line = JSON.parse(response.body).last
+ lines = JSON.parse(response.body)
- expect(match_line['type']).to be_nil
- end
+ expect(lines.size).to eq(diff_lines.size)
end
end
end
@@ -305,7 +289,7 @@ describe Projects::BlobController do
merge_request.update!(source_project: other_project, target_project: other_project)
end
- it "it redirect to blob" do
+ it "redirects to blob" do
put :update, params: mr_params
expect(response).to redirect_to(blob_after_edit_path)
diff --git a/spec/controllers/projects/boards_controller_spec.rb b/spec/controllers/projects/boards_controller_spec.rb
index 09199067024..ae85000b4e0 100644
--- a/spec/controllers/projects/boards_controller_spec.rb
+++ b/spec/controllers/projects/boards_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::BoardsController do
@@ -28,28 +30,6 @@ describe Projects::BoardsController do
expect(response.content_type).to eq 'text/html'
end
- it 'redirects to latest visited board' do
- board = create(:board, project: project)
- create(:board_project_recent_visit, project: board.project, board: board, user: user)
-
- list_boards
-
- expect(response).to redirect_to(namespace_project_board_path(id: board.id))
- end
-
- it 'renders template if visited board is not found' do
- temporary_board = create(:board, project: project)
- visited = create(:board_project_recent_visit, project: temporary_board.project, board: temporary_board, user: user)
- temporary_board.delete
-
- allow_any_instance_of(Boards::Visits::LatestService).to receive(:execute).and_return(visited)
-
- list_boards
-
- expect(response).to render_template :index
- expect(response.content_type).to eq 'text/html'
- end
-
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true)
@@ -120,6 +100,10 @@ describe Projects::BoardsController do
end
end
+ it_behaves_like 'unauthorized when external service denies access' do
+ subject { list_boards }
+ end
+
def list_boards(format: :html)
get :index, params: {
namespace_id: project.namespace,
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 52a20fa8d07..c778b7888dc 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::BranchesController do
diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb
index cfa010c2d1c..cc6ac83ca38 100644
--- a/spec/controllers/projects/ci/lints_controller_spec.rb
+++ b/spec/controllers/projects/ci/lints_controller_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Ci::LintsController do
+ include StubRequests
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
@@ -16,15 +20,15 @@ describe Projects::Ci::LintsController do
get :show, params: { namespace_id: project.namespace, project_id: project }
end
- it 'should be success' do
+ it 'is success' do
expect(response).to be_success
end
- it 'should render show page' do
+ it 'renders show page' do
expect(response).to render_template :show
end
- it 'should retrieve project' do
+ it 'retrieves project' do
expect(assigns(:project)).to eq(project)
end
end
@@ -36,7 +40,7 @@ describe Projects::Ci::LintsController do
get :show, params: { namespace_id: project.namespace, project_id: project }
end
- it 'should respond with 404' do
+ it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
@@ -68,13 +72,13 @@ describe Projects::Ci::LintsController do
context 'with a valid gitlab-ci.yml' do
before do
- WebMock.stub_request(:get, remote_file_path).to_return(body: remote_file_content)
+ stub_full_request(remote_file_path).to_return(body: remote_file_content)
project.add_developer(user)
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end
- it 'should be success' do
+ it 'is success' do
expect(response).to be_success
end
@@ -82,7 +86,7 @@ describe Projects::Ci::LintsController do
expect(response).to render_template :show
end
- it 'should retrieve project' do
+ it 'retrieves project' do
expect(assigns(:project)).to eq(project)
end
end
@@ -102,7 +106,7 @@ describe Projects::Ci::LintsController do
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end
- it 'should assign errors' do
+ it 'assigns errors' do
expect(assigns[:error]).to eq('jobs:rubocop config contains unknown keys: scriptt')
end
end
@@ -114,7 +118,7 @@ describe Projects::Ci::LintsController do
post :create, params: { namespace_id: project.namespace, project_id: project, content: content }
end
- it 'should respond with 404' do
+ it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/controllers/projects/clusters/applications_controller_spec.rb b/spec/controllers/projects/clusters/applications_controller_spec.rb
index cb558259225..70b34f071c8 100644
--- a/spec/controllers/projects/clusters/applications_controller_spec.rb
+++ b/spec/controllers/projects/clusters/applications_controller_spec.rb
@@ -9,7 +9,22 @@ describe Projects::Clusters::ApplicationsController do
Clusters::Cluster::APPLICATIONS[application]
end
+ shared_examples 'a secure endpoint' do
+ it { expect { subject }.to be_allowed_for(:admin) }
+ it { expect { subject }.to be_allowed_for(:owner).of(project) }
+ it { expect { subject }.to be_allowed_for(:maintainer).of(project) }
+ it { expect { subject }.to be_denied_for(:developer).of(project) }
+ it { expect { subject }.to be_denied_for(:reporter).of(project) }
+ it { expect { subject }.to be_denied_for(:guest).of(project) }
+ it { expect { subject }.to be_denied_for(:user) }
+ it { expect { subject }.to be_denied_for(:external) }
+ end
+
describe 'POST create' do
+ subject do
+ post :create, params: params.merge(namespace_id: project.namespace, project_id: project)
+ end
+
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:project) { cluster.project }
let(:application) { 'helm' }
@@ -26,7 +41,7 @@ describe Projects::Clusters::ApplicationsController do
it 'schedule an application installation' do
expect(ClusterInstallAppWorker).to receive(:perform_async).with(application, anything).once
- expect { go }.to change { current_application.count }
+ expect { subject }.to change { current_application.count }
expect(response).to have_http_status(:no_content)
expect(cluster.application_helm).to be_scheduled
end
@@ -37,7 +52,7 @@ describe Projects::Clusters::ApplicationsController do
end
it 'return 404' do
- expect { go }.not_to change { current_application.count }
+ expect { subject }.not_to change { current_application.count }
expect(response).to have_http_status(:not_found)
end
end
@@ -46,9 +61,7 @@ describe Projects::Clusters::ApplicationsController do
let(:application) { 'unkwnown-app' }
it 'return 404' do
- go
-
- expect(response).to have_http_status(:not_found)
+ is_expected.to have_http_status(:not_found)
end
end
@@ -58,9 +71,7 @@ describe Projects::Clusters::ApplicationsController do
end
it 'returns 400' do
- go
-
- expect(response).to have_http_status(:bad_request)
+ is_expected.to have_http_status(:bad_request)
end
end
end
@@ -70,18 +81,130 @@ describe Projects::Clusters::ApplicationsController do
allow(ClusterInstallAppWorker).to receive(:perform_async)
end
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:maintainer).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
+ it_behaves_like 'a secure endpoint'
+ end
+ end
+
+ describe 'PATCH update' do
+ subject do
+ patch :update, params: params.merge(namespace_id: project.namespace, project_id: project)
end
- def go
- post :create, params: params.merge(namespace_id: project.namespace, project_id: project)
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let!(:application) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+ let(:application_name) { application.name }
+ let(:params) { { application: application_name, id: cluster.id, hostname: "new.example.com" } }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context "when cluster and app exists" do
+ it "schedules an application update" do
+ expect(ClusterPatchAppWorker).to receive(:perform_async).with(application.name, anything).once
+
+ is_expected.to have_http_status(:no_content)
+
+ expect(cluster.application_knative).to be_scheduled
+ end
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is unknown' do
+ let(:application_name) { 'unkwnown-app' }
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is already scheduled' do
+ before do
+ application.make_scheduled!
+ end
+
+ it { is_expected.to have_http_status(:bad_request) }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(ClusterPatchAppWorker).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
+ end
+ end
+
+ describe 'DELETE destroy' do
+ subject do
+ delete :destroy, params: params.merge(namespace_id: project.namespace, project_id: project)
+ end
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+ let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let(:application_name) { application.name }
+ let(:params) { { application: application_name, id: cluster.id } }
+ let(:worker_class) { Clusters::Applications::UninstallWorker }
+
+ describe 'functionality' do
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ context "when cluster and app exists" do
+ it "schedules an application update" do
+ expect(worker_class).to receive(:perform_async).with(application.name, application.id).once
+
+ is_expected.to have_http_status(:no_content)
+
+ expect(cluster.application_prometheus).to be_scheduled
+ end
+ end
+
+ context 'when cluster do not exists' do
+ before do
+ cluster.destroy!
+ end
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is unknown' do
+ let(:application_name) { 'unkwnown-app' }
+
+ it { is_expected.to have_http_status(:not_found) }
+ end
+
+ context 'when application is already scheduled' do
+ before do
+ application.make_scheduled!
+ end
+
+ it { is_expected.to have_http_status(:bad_request) }
+ end
+ end
+
+ describe 'security' do
+ before do
+ allow(worker_class).to receive(:perform_async)
+ end
+
+ it_behaves_like 'a secure endpoint'
end
end
end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index d94c18ddc02..8d37bd82d21 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -165,6 +165,7 @@ describe Projects::ClustersController do
{
cluster: {
name: 'new-cluster',
+ managed: '1',
provider_gcp_attributes: {
gcp_project_id: 'gcp-project-12345',
legacy_abac: legacy_abac_param
@@ -191,6 +192,7 @@ describe Projects::ClustersController do
expect(project.clusters.first).to be_gcp
expect(project.clusters.first).to be_kubernetes
expect(project.clusters.first.provider_gcp).to be_legacy_abac
+ expect(project.clusters.first.managed?).to be_truthy
end
context 'when legacy_abac param is false' do
@@ -251,6 +253,7 @@ describe Projects::ClustersController do
{
cluster: {
name: 'new-cluster',
+ managed: '1',
platform_kubernetes_attributes: {
api_url: 'http://my-url',
token: 'test',
@@ -302,9 +305,35 @@ describe Projects::ClustersController do
expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
- expect(project.clusters.first).to be_user
- expect(project.clusters.first).to be_kubernetes
- expect(project.clusters.first).to be_platform_kubernetes_rbac
+ cluster = project.clusters.first
+
+ expect(cluster).to be_user
+ expect(cluster).to be_kubernetes
+ expect(cluster).to be_platform_kubernetes_rbac
+ end
+ end
+
+ context 'when creates a user-managed cluster' do
+ let(:params) do
+ {
+ cluster: {
+ name: 'new-cluster',
+ managed: '0',
+ platform_kubernetes_attributes: {
+ api_url: 'http://my-url',
+ token: 'test',
+ namespace: 'aaa',
+ authorization_type: 'rbac'
+ }
+ }
+ }
+ end
+
+ it 'creates a new user-managed cluster' do
+ go
+ cluster = project.clusters.first
+
+ expect(cluster.managed?).to be_falsy
end
end
end
diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb
index 19cac47325c..b5c6382a26d 100644
--- a/spec/controllers/projects/commit_controller_spec.rb
+++ b/spec/controllers/projects/commit_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CommitController do
diff --git a/spec/controllers/projects/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb
index 8cb9130b834..9db1ac2a46c 100644
--- a/spec/controllers/projects/commits_controller_spec.rb
+++ b/spec/controllers/projects/commits_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CommitsController do
@@ -15,7 +17,7 @@ describe Projects::CommitsController do
describe "GET commits_root" do
context "no ref is provided" do
- it 'should redirect to the default branch of the project' do
+ it 'redirects to the default branch of the project' do
get(:commits_root,
params: {
namespace_id: project.namespace,
@@ -113,6 +115,8 @@ describe Projects::CommitsController do
render_views
before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original unless id.include?(' ')
+
get(:signatures,
params: {
namespace_id: project.namespace,
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index cfd70e93efb..92380a2bf09 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CompareController do
@@ -80,7 +82,7 @@ describe Projects::CompareController do
show_request
expect(response).to be_success
- expect(assigns(:diffs).diff_files.to_a).to eq([])
+ expect(assigns(:diffs)).to eq([])
expect(assigns(:commits)).to eq([])
end
end
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 6a63cbdf8e2..2dc97e18113 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CycleAnalyticsController do
diff --git a/spec/controllers/projects/deploy_keys_controller_spec.rb b/spec/controllers/projects/deploy_keys_controller_spec.rb
index e54cf3e8181..fcd14f13863 100644
--- a/spec/controllers/projects/deploy_keys_controller_spec.rb
+++ b/spec/controllers/projects/deploy_keys_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::DeployKeysController do
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index 5c33098fd31..95417936df4 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::DeploymentsController do
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index 0b9f336cf13..4c29162cd0f 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::DiscussionsController do
diff --git a/spec/controllers/projects/environments/prometheus_api_controller_spec.rb b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
new file mode 100644
index 00000000000..d232408b775
--- /dev/null
+++ b/spec/controllers/projects/environments/prometheus_api_controller_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::Environments::PrometheusApiController do
+ set(:project) { create(:project) }
+ set(:environment) { create(:environment, project: project) }
+ set(:user) { create(:user) }
+
+ before do
+ project.add_reporter(user)
+ sign_in(user)
+ end
+
+ describe 'GET #proxy' do
+ let(:prometheus_proxy_service) { instance_double(Prometheus::ProxyService) }
+
+ let(:expected_params) do
+ ActionController::Parameters.new(
+ environment_params(
+ proxy_path: 'query',
+ controller: 'projects/environments/prometheus_api',
+ action: 'proxy'
+ )
+ ).permit!
+ end
+
+ context 'with valid requests' do
+ before do
+ allow(Prometheus::ProxyService).to receive(:new)
+ .with(environment, 'GET', 'query', expected_params)
+ .and_return(prometheus_proxy_service)
+
+ allow(prometheus_proxy_service).to receive(:execute)
+ .and_return(service_result)
+ end
+
+ context 'with success result' do
+ let(:service_result) { { status: :success, body: prometheus_body } }
+ let(:prometheus_body) { '{"status":"success"}' }
+ let(:prometheus_json_body) { JSON.parse(prometheus_body) }
+
+ it 'returns prometheus response' do
+ get :proxy, params: environment_params
+
+ expect(Prometheus::ProxyService).to have_received(:new)
+ .with(environment, 'GET', 'query', expected_params)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(prometheus_json_body)
+ end
+
+ context 'with format string' do
+ before do
+ expected_params[:query] = %{up{environment="#{environment.slug}"}}
+ end
+
+ it 'replaces variables with values' do
+ get :proxy, params: environment_params.merge(query: 'up{environment="%{ci_environment_slug}"}')
+
+ expect(Prometheus::ProxyService).to have_received(:new)
+ .with(environment, 'GET', 'query', expected_params)
+ end
+
+ context 'with nil query' do
+ let(:params_without_query) do
+ params = environment_params
+ params.delete(:query)
+ params
+ end
+
+ before do
+ expected_params.delete(:query)
+ end
+
+ it 'does not raise error' do
+ get :proxy, params: params_without_query
+
+ expect(Prometheus::ProxyService).to have_received(:new)
+ .with(environment, 'GET', 'query', expected_params)
+ end
+ end
+ end
+ end
+
+ context 'with nil result' do
+ let(:service_result) { nil }
+
+ it 'returns 202 accepted' do
+ get :proxy, params: environment_params
+
+ expect(json_response['status']).to eq('processing')
+ expect(json_response['message']).to eq('Not ready yet. Try again later.')
+ expect(response).to have_gitlab_http_status(:accepted)
+ end
+ end
+
+ context 'with 404 result' do
+ let(:service_result) { { http_status: 404, status: :success, body: '{"body": "value"}' } }
+
+ it 'returns body' do
+ get :proxy, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect(json_response['body']).to eq('value')
+ end
+ end
+
+ context 'with error result' do
+ context 'with http_status' do
+ let(:service_result) do
+ { http_status: :service_unavailable, status: :error, message: 'error message' }
+ end
+
+ it 'sets the http response status code' do
+ get :proxy, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:service_unavailable)
+ expect(json_response['status']).to eq('error')
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+
+ context 'without http_status' do
+ let(:service_result) { { status: :error, message: 'error message' } }
+
+ it 'returns bad_request' do
+ get :proxy, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['status']).to eq('error')
+ expect(json_response['message']).to eq('error message')
+ end
+ end
+ end
+ end
+
+ context 'with inappropriate requests' do
+ context 'with anonymous user' do
+ before do
+ sign_out(user)
+ end
+
+ it 'redirects to signin page' do
+ get :proxy, params: environment_params
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'without correct permissions' do
+ before do
+ project.team.truncate
+ end
+
+ it 'returns 404' do
+ get :proxy, params: environment_params
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'with invalid environment id' do
+ let(:other_environment) { create(:environment) }
+
+ it 'returns 404' do
+ get :proxy, params: environment_params(id: other_environment.id)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ private
+
+ def environment_params(params = {})
+ {
+ id: environment.id.to_s,
+ namespace_id: project.namespace.name,
+ project_id: project.name,
+ proxy_path: 'query',
+ query: '1'
+ }.merge(params)
+ end
+end
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index aa97a417a98..9699f2952f2 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::EnvironmentsController do
@@ -54,9 +56,9 @@ describe Projects::EnvironmentsController do
it 'responds with a flat payload describing available environments' do
expect(environments.count).to eq 3
- expect(environments.first['name']).to eq 'production'
- expect(environments.second['name']).to eq 'staging/review-1'
- expect(environments.third['name']).to eq 'staging/review-2'
+ expect(environments.first).to include('name' => 'production', 'name_without_type' => 'production')
+ expect(environments.second).to include('name' => 'staging/review-1', 'name_without_type' => 'review-1')
+ expect(environments.third).to include('name' => 'staging/review-2', 'name_without_type' => 'review-2')
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
@@ -155,9 +157,9 @@ describe Projects::EnvironmentsController do
expect(response).to be_ok
expect(response).not_to render_template 'folder'
expect(json_response['environments'][0])
- .to include('name' => 'staging-1.0/review')
+ .to include('name' => 'staging-1.0/review', 'name_without_type' => 'review')
expect(json_response['environments'][1])
- .to include('name' => 'staging-1.0/zzz')
+ .to include('name' => 'staging-1.0/zzz', 'name_without_type' => 'zzz')
end
end
end
@@ -283,7 +285,7 @@ describe Projects::EnvironmentsController do
.and_return([:fake_terminal])
expect(Gitlab::Workhorse)
- .to receive(:terminal_websocket)
+ .to receive(:channel_websocket)
.with(:fake_terminal)
.and_return(workhorse: :response)
@@ -340,11 +342,9 @@ describe Projects::EnvironmentsController do
end
context 'when environment has no metrics' do
- before do
- expect(environment).to receive(:metrics).and_return(nil)
- end
-
it 'returns a metrics page' do
+ expect(environment).not_to receive(:metrics)
+
get :metrics, params: environment_params
expect(response).to be_ok
@@ -352,6 +352,8 @@ describe Projects::EnvironmentsController do
context 'when requesting metrics as JSON' do
it 'returns a metrics JSON document' do
+ expect(environment).to receive(:metrics).and_return(nil)
+
get :metrics, params: environment_params(format: :json)
expect(response).to have_gitlab_http_status(204)
@@ -381,6 +383,8 @@ describe Projects::EnvironmentsController do
end
describe 'GET #additional_metrics' do
+ let(:window_params) { { start: '1554702993.5398998', end: '1554717396.996232' } }
+
before do
allow(controller).to receive(:environment).and_return(environment)
end
@@ -392,7 +396,7 @@ describe Projects::EnvironmentsController do
context 'when requesting metrics as JSON' do
it 'returns a metrics JSON document' do
- get :additional_metrics, params: environment_params(format: :json)
+ additional_metrics(window_params)
expect(response).to have_gitlab_http_status(204)
expect(json_response).to eq({})
@@ -412,7 +416,7 @@ describe Projects::EnvironmentsController do
end
it 'returns a metrics JSON document' do
- get :additional_metrics, params: environment_params(format: :json)
+ additional_metrics(window_params)
expect(response).to be_ok
expect(json_response['success']).to be(true)
@@ -420,6 +424,139 @@ describe Projects::EnvironmentsController do
expect(json_response['last_update']).to eq(42)
end
end
+
+ context 'when time params are missing' do
+ it 'raises an error when window params are missing' do
+ expect { additional_metrics }
+ .to raise_error(ActionController::ParameterMissing)
+ end
+ end
+
+ context 'when only one time param is provided' do
+ it 'raises an error when start is missing' do
+ expect { additional_metrics(end: '1552647300.651094') }
+ .to raise_error(ActionController::ParameterMissing)
+ end
+
+ it 'raises an error when end is missing' do
+ expect { additional_metrics(start: '1552647300.651094') }
+ .to raise_error(ActionController::ParameterMissing)
+ end
+ end
+ end
+
+ describe 'metrics_dashboard' do
+ context 'when prometheus endpoint is disabled' do
+ before do
+ stub_feature_flags(environment_metrics_use_prometheus_endpoint: false)
+ end
+
+ it 'responds with status code 403' do
+ get :metrics_dashboard, params: environment_params(format: :json)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ shared_examples_for '200 response' do |contains_all_dashboards: false|
+ let(:expected_keys) { %w(dashboard status) }
+
+ before do
+ expected_keys << 'all_dashboards' if contains_all_dashboards
+ end
+
+ it 'returns a json representation of the environment dashboard' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response.keys).to contain_exactly(*expected_keys)
+ expect(json_response['dashboard']).to be_an_instance_of(Hash)
+ end
+ end
+
+ shared_examples_for 'error response' do |status_code, contains_all_dashboards: false|
+ let(:expected_keys) { %w(message status) }
+
+ before do
+ expected_keys << 'all_dashboards' if contains_all_dashboards
+ end
+
+ it 'returns an error response' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(response).to have_gitlab_http_status(status_code)
+ expect(json_response.keys).to contain_exactly(*expected_keys)
+ end
+ end
+
+ shared_examples_for 'has all dashboards' do
+ it 'includes an index of all available dashboards' do
+ get :metrics_dashboard, params: environment_params(dashboard_params)
+
+ expect(json_response.keys).to include('all_dashboards')
+ expect(json_response['all_dashboards']).to be_an_instance_of(Array)
+ expect(json_response['all_dashboards']).to all( include('path', 'default') )
+ end
+ end
+
+ context 'when multiple dashboards is disabled' do
+ before do
+ stub_feature_flags(environment_metrics_show_multiple_dashboards: false)
+ end
+
+ let(:dashboard_params) { { format: :json } }
+
+ it_behaves_like '200 response'
+
+ context 'when the dashboard could not be provided' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it_behaves_like 'error response', :unprocessable_entity
+ end
+
+ context 'when a dashboard param is specified' do
+ let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/not_there_dashboard.yml' } }
+
+ it_behaves_like '200 response'
+ end
+ end
+
+ context 'when multiple dashboards is enabled' do
+ let(:dashboard_params) { { format: :json } }
+
+ it_behaves_like '200 response', contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+
+ context 'when a dashboard could not be provided' do
+ before do
+ allow(YAML).to receive(:safe_load).and_return({})
+ end
+
+ it_behaves_like 'error response', :unprocessable_entity, contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+
+ context 'when a dashboard param is specified' do
+ let(:dashboard_params) { { format: :json, dashboard: '.gitlab/dashboards/test.yml' } }
+
+ context 'when the dashboard is available' do
+ let(:dashboard_yml) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+ let(:dashboard_file) { { '.gitlab/dashboards/test.yml' => dashboard_yml } }
+ let(:project) { create(:project, :custom_repo, files: dashboard_file) }
+ let(:environment) { create(:environment, name: 'production', project: project) }
+
+ it_behaves_like '200 response', contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+
+ context 'when the dashboard does not exist' do
+ it_behaves_like 'error response', :not_found, contains_all_dashboards: true
+ it_behaves_like 'has all dashboards'
+ end
+ end
+ end
end
describe 'GET #search' do
@@ -500,4 +637,8 @@ describe Projects::EnvironmentsController do
project_id: project,
id: environment.id)
end
+
+ def additional_metrics(opts = {})
+ get :additional_metrics, params: environment_params(format: :json, **opts)
+ end
end
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
index 9072d67af07..538dbb5ad0b 100644
--- a/spec/controllers/projects/find_file_controller_spec.rb
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::FindFileController do
diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb
index 0e1663c8585..3423fdf4c41 100644
--- a/spec/controllers/projects/forks_controller_spec.rb
+++ b/spec/controllers/projects/forks_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ForksController do
diff --git a/spec/controllers/projects/git_http_controller_spec.rb b/spec/controllers/projects/git_http_controller_spec.rb
new file mode 100644
index 00000000000..bf099e8deeb
--- /dev/null
+++ b/spec/controllers/projects/git_http_controller_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::GitHttpController do
+ describe 'HEAD #info_refs' do
+ it 'returns 403' do
+ project = create(:project, :public, :repository)
+
+ head :info_refs, params: { namespace_id: project.namespace.to_param, project_id: project.path + '.git' }
+
+ expect(response.status).to eq(403)
+ end
+ end
+end
diff --git a/spec/controllers/projects/graphs_controller_spec.rb b/spec/controllers/projects/graphs_controller_spec.rb
index 8decd8f1382..b5248c7f0c8 100644
--- a/spec/controllers/projects/graphs_controller_spec.rb
+++ b/spec/controllers/projects/graphs_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::GraphsController do
@@ -26,7 +28,23 @@ describe Projects::GraphsController do
end
describe 'charts' do
+ context 'with an anonymous user' do
+ let(:project) { create(:project, :repository, :public) }
+
+ before do
+ sign_out(user)
+ end
+
+ it 'renders charts with 200 status code' do
+ get(:charts, params: { namespace_id: project.namespace.path, project_id: project.path, id: 'master' })
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:charts)
+ end
+ end
+
context 'when languages were previously detected' do
+ let(:project) { create(:project, :repository, detected_repository_languages: true) }
let!(:repository_language) { create(:repository_language, project: project) }
it 'sets the languages properly' do
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index 675eeff8d12..d0cb3a74b78 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::GroupLinksController do
@@ -65,8 +67,24 @@ describe Projects::GroupLinksController do
end
end
+ context 'when user does not have access to the public group' do
+ let(:group) { create(:group, :public) }
+
+ include_context 'link project to group'
+
+ it 'renders 404' do
+ expect(response.status).to eq 404
+ end
+
+ it 'does not share project with that group' do
+ expect(group.shared_projects).not_to include project
+ end
+ end
+
context 'when project group id equal link group id' do
before do
+ group2.add_developer(user)
+
post(:create, params: {
namespace_id: project.namespace,
project_id: project,
@@ -102,5 +120,26 @@ describe Projects::GroupLinksController do
expect(flash[:alert]).to eq('Please select a group.')
end
end
+
+ context 'when link is not persisted in the database' do
+ before do
+ allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute)
+ .and_return({ status: :error, http_status: 409, message: 'error' })
+
+ post(:create, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ link_group_id: group.id,
+ link_group_access: ProjectGroupLink.default_access
+ })
+ end
+
+ it 'redirects to project group links page' do
+ expect(response).to redirect_to(
+ project_project_members_path(project)
+ )
+ expect(flash[:alert]).to eq('error')
+ end
+ end
end
end
diff --git a/spec/controllers/projects/hooks_controller_spec.rb b/spec/controllers/projects/hooks_controller_spec.rb
index 3037c922b68..137296b4f19 100644
--- a/spec/controllers/projects/hooks_controller_spec.rb
+++ b/spec/controllers/projects/hooks_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::HooksController do
diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb
index 3ebfe4b0918..bdc81efe3bc 100644
--- a/spec/controllers/projects/imports_controller_spec.rb
+++ b/spec/controllers/projects/imports_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ImportsController do
@@ -120,4 +122,19 @@ describe Projects::ImportsController do
end
end
end
+
+ describe 'POST #create' do
+ let(:params) { { import_url: 'https://github.com/vim/vim.git', import_url_user: 'user', import_url_password: 'password' } }
+ let(:project) { create(:project) }
+
+ before do
+ allow(RepositoryImportWorker).to receive(:perform_async)
+
+ post :create, params: { project: params, namespace_id: project.namespace.to_param, project_id: project }
+ end
+
+ it 'sets import_url to the project' do
+ expect(project.reload.import_url).to eq('https://user:password@github.com/vim/vim.git')
+ end
+ end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index c34d7c13d57..32607fc5f56 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::IssuesController do
@@ -127,6 +129,48 @@ describe Projects::IssuesController do
expect(assigns(:issues).size).to eq(2)
end
end
+
+ context 'with relative_position sorting' do
+ let!(:issue_list) { create_list(:issue, 2, project: project) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ allow(Kaminari.config).to receive(:default_per_page).and_return(1)
+ end
+
+ it 'overrides the number allowed on the page' do
+ get :index,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ sort: 'relative_position'
+ }
+
+ expect(assigns(:issues).count).to eq 2
+ end
+
+ it 'allows the default number on the page' do
+ get :index,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ expect(assigns(:issues).count).to eq 1
+ end
+ end
+
+ context 'external authorization' do
+ before do
+ sign_in user
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'unauthorized when external service denies access' do
+ subject { get :index, params: { namespace_id: project.namespace, project_id: project } }
+ end
+ end
end
describe 'GET #new' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index d8a331b3cf0..490e9841492 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -1,4 +1,5 @@
# coding: utf-8
+# frozen_string_literal: true
require 'spec_helper'
describe Projects::JobsController, :clean_gitlab_redis_shared_state do
@@ -100,7 +101,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
- describe 'GET show' do
+ describe 'GET show', :request_store do
let!(:job) { create(:ci_build, :failed, pipeline: pipeline) }
let!(:second_job) { create(:ci_build, :failed, pipeline: pipeline) }
let!(:third_job) { create(:ci_build, :failed) }
@@ -142,13 +143,24 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
project.add_developer(user)
sign_in(user)
- allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request)
+ allow_any_instance_of(Ci::Build)
+ .to receive(:merge_request)
+ .and_return(merge_request)
+ end
+
+ it 'does not serialize builds in exposed stages' do
+ get_show_json
- get_show(id: job.id, format: :json)
+ json_response.dig('pipeline', 'details', 'stages').tap do |stages|
+ expect(stages.map(&:keys).flatten)
+ .to eq %w[name title status path dropdown_path]
+ end
end
context 'when job failed' do
it 'exposes needed information' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
@@ -158,6 +170,10 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'when job is running' do
+ before do
+ get_show_json
+ end
+
context 'job is cancelable' do
let(:job) { create(:ci_build, :running, pipeline: pipeline) }
@@ -180,6 +196,10 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'when job has artifacts' do
+ before do
+ get_show_json
+ end
+
context 'with not expiry date' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
@@ -211,6 +231,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
it 'exposes empty state illustrations' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['status']['illustration']).to have_key('image')
@@ -223,6 +245,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:job) { create(:ci_build, :success, pipeline: pipeline) }
it 'does not exposes the deployment information' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['deployment_status']).to be_nil
end
@@ -233,11 +257,20 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:environment) { create(:environment, project: project, name: 'staging', state: :available) }
let(:job) { create(:ci_build, :running, environment: environment.name, pipeline: pipeline) }
+ before do
+ create(:deployment, :success, environment: environment, project: project)
+ end
+
it 'exposes the deployment information' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to match_schema('job/job_details')
- expect(json_response['deployment_status']["status"]).to eq 'creating'
- expect(json_response['deployment_status']["environment"]).not_to be_nil
+ expect(json_response.dig('deployment_status', 'status')).to eq 'creating'
+ expect(json_response.dig('deployment_status', 'environment')).not_to be_nil
+ expect(json_response.dig('deployment_status', 'environment', 'last_deployment')).not_to be_nil
+ expect(json_response.dig('deployment_status', 'environment', 'last_deployment'))
+ .not_to include('commit')
end
end
@@ -249,11 +282,11 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
before do
project.add_maintainer(user)
sign_in(user)
-
- get_show(id: job.id, format: :json)
end
it 'user can edit runner' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['runner']).to have_key('edit_path')
@@ -269,11 +302,11 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
before do
project.add_maintainer(user)
sign_in(user)
-
- get_show(id: job.id, format: :json)
end
it 'user can not edit runner' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['runner']).not_to have_key('edit_path')
@@ -288,11 +321,11 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
before do
project.add_maintainer(user)
sign_in(user)
-
- get_show(id: job.id, format: :json)
end
it 'user can not edit runner' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['runner']).not_to have_key('edit_path')
@@ -305,6 +338,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) }
it 'exposes needed information' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['runners']['online']).to be false
@@ -318,6 +353,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) }
it 'exposes needed information' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['runners']['online']).to be false
@@ -327,6 +364,10 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
context 'settings_path' do
+ before do
+ get_show_json
+ end
+
context 'when user is developer' do
it 'settings_path is not available' do
expect(response).to have_gitlab_http_status(:ok)
@@ -353,6 +394,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'when no trace is available' do
it 'has_trace is false' do
+ get_show_json
+
expect(response).to match_response_schema('job/job_details')
expect(json_response['has_trace']).to be false
end
@@ -362,17 +405,21 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
let(:job) { create(:ci_build, :running, :trace_live, pipeline: pipeline) }
it "has_trace is true" do
+ get_show_json
+
expect(response).to match_response_schema('job/job_details')
expect(json_response['has_trace']).to be true
end
end
it 'exposes the stage the job belongs to' do
+ get_show_json
+
expect(json_response['stage']).to eq('test')
end
end
- context 'when requesting JSON job is triggered' do
+ context 'when requesting triggered job JSON' do
let!(:merge_request) { create(:merge_request, source_project: project) }
let(:trigger) { create(:ci_trigger, project: project) }
let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
@@ -382,15 +429,15 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
project.add_developer(user)
sign_in(user)
- allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request)
+ allow_any_instance_of(Ci::Build)
+ .to receive(:merge_request)
+ .and_return(merge_request)
end
context 'with no variables' do
- before do
- get_show(id: job.id, format: :json)
- end
-
it 'exposes trigger information' do
+ get_show_json
+
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('job/job_details')
expect(json_response['trigger']['short_token']).to eq 'toke'
@@ -407,7 +454,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
before do
project.add_maintainer(user)
- get_show(id: job.id, format: :json)
+ get_show_json
end
it 'returns a job_detail' do
@@ -431,7 +478,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'user is not a mantainer' do
before do
- get_show(id: job.id, format: :json)
+ get_show_json
end
it 'returns a job_detail' do
@@ -455,6 +502,11 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
+ def get_show_json
+ expect { get_show(id: job.id, format: :json) }
+ .not_to change { Gitlab::GitalyClient.get_request_count }
+ end
+
def get_show(**extra_params)
params = {
namespace_id: project.namespace.to_param,
@@ -789,8 +841,8 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
it 'erases artifacts' do
- expect(job.artifacts_file.exists?).to be_falsey
- expect(job.artifacts_metadata.exists?).to be_falsey
+ expect(job.artifacts_file.present?).to be_falsey
+ expect(job.artifacts_metadata.present?).to be_falsey
end
it 'erases trace' do
@@ -989,7 +1041,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
context 'and valid id' do
it 'returns the terminal for the job' do
expect(Gitlab::Workhorse)
- .to receive(:terminal_websocket)
+ .to receive(:channel_websocket)
.and_return(workhorse: :response)
get_terminal_websocket(id: job.id)
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 32897a0f1b4..ff089df37f7 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::LabelsController do
diff --git a/spec/controllers/projects/mattermosts_controller_spec.rb b/spec/controllers/projects/mattermosts_controller_spec.rb
index 6c8c7cd8f2b..45125385d9e 100644
--- a/spec/controllers/projects/mattermosts_controller_spec.rb
+++ b/spec/controllers/projects/mattermosts_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MattermostsController do
diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
index 039f35875d2..8e4ac64f7b0 100644
--- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MergeRequests::ConflictsController do
diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
index f031a74c5bd..5fefad86ef3 100644
--- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MergeRequests::CreationsController do
diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
index e85f32d6e30..13a28b738ca 100644
--- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MergeRequests::DiffsController do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 79f97aa4170..f8c0ab55eb4 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MergeRequestsController do
@@ -60,6 +62,8 @@ describe Projects::MergeRequestsController do
end
it "renders merge request page" do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
go(format: :html)
expect(response).to be_success
@@ -86,6 +90,10 @@ describe Projects::MergeRequestsController do
end
describe 'as json' do
+ before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+ end
+
context 'with basic serializer param' do
it 'renders basic MR entity as json' do
go(serializer: 'basic', format: :json)
@@ -232,11 +240,11 @@ describe Projects::MergeRequestsController do
assignee = create(:user)
project.add_developer(assignee)
- update_merge_request({ assignee_id: assignee.id }, format: :json)
+ update_merge_request({ assignee_ids: [assignee.id] }, format: :json)
+
body = JSON.parse(response.body)
- expect(body['assignee'].keys)
- .to match_array(%w(name username avatar_url id state web_url))
+ expect(body['assignees']).to all(include(*%w(name username avatar_url id state web_url)))
end
end
@@ -421,8 +429,9 @@ describe Projects::MergeRequestsController do
it 'sets the MR to merge when the pipeline succeeds' do
service = double(:merge_when_pipeline_succeeds_service)
+ allow(service).to receive(:available_for?) { true }
- expect(MergeRequests::MergeWhenPipelineSucceedsService)
+ expect(AutoMerge::MergeWhenPipelineSucceedsService)
.to receive(:new).with(project, anything, anything)
.and_return(service)
expect(service).to receive(:execute).with(merge_request)
@@ -705,9 +714,9 @@ describe Projects::MergeRequestsController do
end
end
- describe 'POST cancel_merge_when_pipeline_succeeds' do
+ describe 'POST cancel_auto_merge' do
subject do
- post :cancel_merge_when_pipeline_succeeds,
+ post :cancel_auto_merge,
params: {
format: :json,
namespace_id: merge_request.project.namespace.to_param,
@@ -717,14 +726,15 @@ describe Projects::MergeRequestsController do
xhr: true
end
- it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
- mwps_service = double
+ it 'calls AutoMergeService' do
+ auto_merge_service = double
- allow(MergeRequests::MergeWhenPipelineSucceedsService)
+ allow(AutoMergeService)
.to receive(:new)
- .and_return(mwps_service)
+ .and_return(auto_merge_service)
- expect(mwps_service).to receive(:cancel).with(merge_request)
+ allow(auto_merge_service).to receive(:available_strategies).with(merge_request)
+ expect(auto_merge_service).to receive(:cancel).with(merge_request)
subject
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index ac54b3c3952..767cee7d54a 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MilestonesController do
@@ -173,6 +175,40 @@ describe Projects::MilestonesController do
end
end
+ describe '#labels' do
+ render_views
+
+ context 'as json' do
+ let!(:guest) { create(:user, username: 'guest1') }
+ let!(:group) { create(:group, :public) }
+ let!(:project) { create(:project, :public, group: group) }
+ let!(:label) { create(:label, title: 'test_label_on_private_issue', project: project) }
+ let!(:confidential_issue) { create(:labeled_issue, confidential: true, project: project, milestone: milestone, labels: [label]) }
+
+ it 'does not render labels of private issues if user has no access' do
+ sign_in(guest)
+
+ get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+
+ expect(json_response['html']).not_to include(label.title)
+ end
+
+ it 'does render labels of private issues if user has access' do
+ sign_in(user)
+
+ get :labels, params: { namespace_id: group.id, project_id: project.id, id: milestone.iid }, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.content_type).to eq 'application/json'
+
+ expect(json_response['html']).to include(label.title)
+ end
+ end
+ end
+
context 'promotion succeeds' do
before do
group.add_developer(user)
diff --git a/spec/controllers/projects/mirrors_controller_spec.rb b/spec/controllers/projects/mirrors_controller_spec.rb
index 86a12a5e903..51ce9e2544f 100644
--- a/spec/controllers/projects/mirrors_controller_spec.rb
+++ b/spec/controllers/projects/mirrors_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MirrorsController do
@@ -65,7 +67,7 @@ describe Projects::MirrorsController do
expect(flash[:notice]).to match(/successfully updated/)
end
- it 'should create a RemoteMirror object' do
+ it 'creates a RemoteMirror object' do
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.to change(RemoteMirror, :count).by(1)
end
end
@@ -79,10 +81,10 @@ describe Projects::MirrorsController do
do_put(project, remote_mirrors_attributes: remote_mirror_attributes)
expect(response).to redirect_to(project_settings_repository_path(project, anchor: 'js-push-remote-settings'))
- expect(flash[:alert]).to match(/Only allowed protocols are/)
+ expect(flash[:alert]).to match(/Only allowed schemes are/)
end
- it 'should not create a RemoteMirror object' do
+ it 'does not create a RemoteMirror object' do
expect { do_put(project, remote_mirrors_attributes: remote_mirror_attributes) }.not_to change(RemoteMirror, :count)
end
end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index 0b0f5117784..6ec84f5c528 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::NotesController do
@@ -413,6 +415,37 @@ describe Projects::NotesController do
end
end
end
+
+ context 'when creating a note with quick actions' do
+ context 'with commands that return changes' do
+ let(:note_text) { "/award :thumbsup:\n/estimate 1d\n/spend 3h" }
+
+ it 'includes changes in commands_changes ' do
+ post :create, params: request_params.merge(note: { note: note_text }, format: :json)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['commands_changes']).to include('emoji_award', 'time_estimate', 'spend_time')
+ expect(json_response['commands_changes']).not_to include('target_project', 'title')
+ end
+ end
+
+ context 'with commands that do not return changes' do
+ let(:issue) { create(:issue, project: project) }
+ let(:other_project) { create(:project) }
+ let(:note_text) { "/move #{other_project.full_path}\n/title AAA" }
+
+ before do
+ other_project.add_developer(user)
+ end
+
+ it 'does not include changes in commands_changes' do
+ post :create, params: request_params.merge(note: { note: note_text }, target_type: 'issue', target_id: issue.id, format: :json)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['commands_changes']).not_to include('target_project', 'title')
+ end
+ end
+ end
end
describe 'PUT update' do
diff --git a/spec/controllers/projects/pages_controller_spec.rb b/spec/controllers/projects/pages_controller_spec.rb
index d6eece47804..f80bbf0d78f 100644
--- a/spec/controllers/projects/pages_controller_spec.rb
+++ b/spec/controllers/projects/pages_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::PagesController do
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index ffb9867a203..ff3afd51cd8 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::PagesDomainsController do
diff --git a/spec/controllers/projects/pipeline_schedules_controller_spec.rb b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
index fa732437fc1..850ef9c92fb 100644
--- a/spec/controllers/projects/pipeline_schedules_controller_spec.rb
+++ b/spec/controllers/projects/pipeline_schedules_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::PipelineSchedulesController do
@@ -89,7 +91,7 @@ describe Projects::PipelineSchedulesController do
context 'when variables_attributes has one variable' do
let(:schedule) do
basic_param.merge({
- variables_attributes: [{ key: 'AAA', secret_value: 'AAA123' }]
+ variables_attributes: [{ key: 'AAA', secret_value: 'AAA123', variable_type: 'file' }]
})
end
@@ -103,6 +105,7 @@ describe Projects::PipelineSchedulesController do
Ci::PipelineScheduleVariable.last.tap do |v|
expect(v.key).to eq("AAA")
expect(v.value).to eq("AAA123")
+ expect(v.variable_type).to eq("file")
end
end
end
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index ece8532cb84..9a50ea79f5e 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::PipelinesController do
@@ -28,6 +30,8 @@ describe Projects::PipelinesController do
end
it 'returns serialized pipelines', :request_store do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
queries = ActiveRecord::QueryRecorder.new do
get_pipelines_index_json
end
@@ -95,6 +99,8 @@ describe Projects::PipelinesController do
RequestStore.clear!
RequestStore.begin!
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
expect { get_pipelines_index_json }
.to change { Gitlab::GitalyClient.get_request_count }.by(2)
end
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb
index 269f105bed2..3656b4e3771 100644
--- a/spec/controllers/projects/pipelines_settings_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::PipelinesSettingsController do
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 3cc3fe69fba..4141e41c7a7 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require('spec_helper')
describe Projects::ProjectMembersController do
@@ -5,7 +7,7 @@ describe Projects::ProjectMembersController do
let(:project) { create(:project, :public, :access_requestable) }
describe 'GET index' do
- it 'should have the project_members address with a 200 status code' do
+ it 'has the project_members address with a 200 status code' do
get :index, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(200)
diff --git a/spec/controllers/projects/prometheus/metrics_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
index 635763ce1d3..17f9483be98 100644
--- a/spec/controllers/projects/prometheus/metrics_controller_spec.rb
+++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Prometheus::MetricsController do
diff --git a/spec/controllers/projects/protected_branches_controller_spec.rb b/spec/controllers/projects/protected_branches_controller_spec.rb
index 483d3bbc37c..0ebbb4b581f 100644
--- a/spec/controllers/projects/protected_branches_controller_spec.rb
+++ b/spec/controllers/projects/protected_branches_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require('spec_helper')
describe Projects::ProtectedBranchesController do
diff --git a/spec/controllers/projects/protected_tags_controller_spec.rb b/spec/controllers/projects/protected_tags_controller_spec.rb
index 1553e081dee..a900947d82e 100644
--- a/spec/controllers/projects/protected_tags_controller_spec.rb
+++ b/spec/controllers/projects/protected_tags_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require('spec_helper')
describe Projects::ProtectedTagsController do
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index cffdf30da6b..97acd47b4da 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::RawController do
@@ -40,7 +42,7 @@ describe Projects::RawController do
end
end
- it_behaves_like 'repository lfs file load' do
+ it_behaves_like 'a controller that can serve LFS files' do
let(:filename) { 'lfs_object.iso' }
let(:filepath) { "be93687/files/lfs/#{filename}" }
end
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
index 62f2af947e4..6db98f2428b 100644
--- a/spec/controllers/projects/refs_controller_spec.rb
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::RefsController do
@@ -44,11 +46,15 @@ describe Projects::RefsController do
end
it 'renders JS' do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
xhr_get(:js)
expect(response).to be_success
end
it 'renders JSON' do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
xhr_get(:json)
expect(response).to be_success
diff --git a/spec/controllers/projects/registry/repositories_controller_spec.rb b/spec/controllers/projects/registry/repositories_controller_spec.rb
index eca187af33d..63d84b366d3 100644
--- a/spec/controllers/projects/registry/repositories_controller_spec.rb
+++ b/spec/controllers/projects/registry/repositories_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Registry::RepositoriesController do
diff --git a/spec/controllers/projects/registry/tags_controller_spec.rb b/spec/controllers/projects/registry/tags_controller_spec.rb
index 74ed89ba1c3..ff35139ae2e 100644
--- a/spec/controllers/projects/registry/tags_controller_spec.rb
+++ b/spec/controllers/projects/registry/tags_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Registry::TagsController do
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 5f4f6f8558f..8fca9e680dd 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe Projects::RepositoriesController do
diff --git a/spec/controllers/projects/runners_controller_spec.rb b/spec/controllers/projects/runners_controller_spec.rb
index 0baaa4e7192..279b4f360c5 100644
--- a/spec/controllers/projects/runners_controller_spec.rb
+++ b/spec/controllers/projects/runners_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::RunnersController do
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 276cf340962..18c594acae0 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -8,9 +8,8 @@ describe Projects::Serverless::FunctionsController do
let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
let(:service) { cluster.platform_kubernetes }
- let(:project) { cluster.project}
+ let(:project) { cluster.project }
let(:namespace) do
create(:cluster_kubernetes_namespace,
@@ -30,17 +29,69 @@ describe Projects::Serverless::FunctionsController do
end
describe 'GET #index' do
- context 'empty cache' do
- it 'has no data' do
+ let(:expected_json) { { 'knative_installed' => knative_state, 'functions' => functions } }
+
+ context 'when cache is being read' do
+ let(:knative_state) { 'checking' }
+ let(:functions) { [] }
+
+ before do
get :index, params: params({ format: :json })
+ end
- expect(response).to have_gitlab_http_status(204)
+ it 'returns checking' do
+ expect(json_response).to eq expected_json
end
- it 'renders an html page' do
- get :index, params: params
+ it { expect(response).to have_gitlab_http_status(200) }
+ end
+
+ context 'when cache is ready' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+ let(:knative_state) { true }
- expect(response).to have_gitlab_http_status(200)
+ before do
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ stub_kubeclient_service_pods(
+ kube_response({ "kind" => "PodList", "items" => [] }),
+ namespace: namespace.namespace
+ )
+ end
+
+ context 'when no functions were found' do
+ let(:functions) { [] }
+
+ before do
+ stub_kubeclient_knative_services(
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ )
+ get :index, params: params({ format: :json })
+ end
+
+ it 'returns checking' do
+ expect(json_response).to eq expected_json
+ end
+
+ it { expect(response).to have_gitlab_http_status(200) }
+ end
+
+ context 'when functions were found' do
+ let(:functions) { ["asdf"] }
+
+ before do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ get :index, params: params({ format: :json })
+ end
+
+ it 'returns functions' do
+ expect(json_response["functions"]).not_to be_empty
+ end
+
+ it { expect(response).to have_gitlab_http_status(200) }
end
end
end
@@ -56,11 +107,12 @@ describe Projects::Serverless::FunctionsController do
context 'valid data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
end
it 'has a valid function name' do
@@ -76,14 +128,24 @@ describe Projects::Serverless::FunctionsController do
end
end
+ describe 'GET #metrics' do
+ context 'invalid data' do
+ it 'has a bad function name' do
+ get :metrics, params: params({ format: :json, environment_id: "*", id: "foo" })
+ expect(response).to have_gitlab_http_status(204)
+ end
+ end
+ end
+
describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
end
it 'has data' do
@@ -91,11 +153,16 @@ describe Projects::Serverless::FunctionsController do
expect(response).to have_gitlab_http_status(200)
- expect(json_response).to contain_exactly(
- a_hash_including(
- "name" => project.name,
- "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
- )
+ expect(json_response).to match(
+ {
+ "knative_installed" => "checking",
+ "functions" => [
+ a_hash_including(
+ "name" => project.name,
+ "url" => "http://#{project.name}.#{namespace.namespace}.example.com"
+ )
+ ]
+ }
)
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 601a292bf54..3608d175d50 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ServicesController do
@@ -147,7 +149,7 @@ describe Projects::ServicesController do
params: { namespace_id: project.namespace, project_id: project, id: service.to_param, service: { namespace: 'updated_namespace' } }
end
- it 'should not update the service' do
+ it 'does not update the service' do
service.reload
expect(service.namespace).not_to eq('updated_namespace')
end
@@ -172,7 +174,7 @@ describe Projects::ServicesController do
context 'with approved services' do
let(:service_id) { 'jira' }
- it 'should render edit page' do
+ it 'renders edit page' do
expect(response).to be_success
end
end
@@ -180,7 +182,7 @@ describe Projects::ServicesController do
context 'with a deprecated service' do
let(:service_id) { 'kubernetes' }
- it 'should render edit page' do
+ it 'renders edit page' do
expect(response).to be_success
end
end
diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
index 41cc0607cee..117b9cf7915 100644
--- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb
+++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require('spec_helper')
describe Projects::Settings::CiCdController do
@@ -107,7 +109,7 @@ describe Projects::Settings::CiCdController do
end
context 'when updating the auto_devops settings' do
- let(:params) { { auto_devops_attributes: { enabled: '', domain: 'mepmep.md' } } }
+ let(:params) { { auto_devops_attributes: { enabled: '' } } }
context 'following the instance default' do
let(:params) { { auto_devops_attributes: { enabled: '' } } }
@@ -189,6 +191,30 @@ describe Projects::Settings::CiCdController do
expect(project.build_timeout).to eq(5400)
end
end
+
+ context 'when build_timeout_human_readable is invalid' do
+ let(:params) { { build_timeout_human_readable: '5m' } }
+
+ it 'set specified timeout' do
+ expect(subject).to set_flash[:alert]
+ expect(response).to redirect_to(namespace_project_settings_ci_cd_path)
+ end
+ end
+
+ context 'when default_git_depth is not specified' do
+ let(:params) { { ci_cd_settings_attributes: { default_git_depth: 10 } } }
+
+ before do
+ project.ci_cd_settings.update!(default_git_depth: nil)
+ end
+
+ it 'set specified git depth' do
+ subject
+
+ project.reload
+ expect(project.default_git_depth).to eq(10)
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/settings/integrations_controller_spec.rb b/spec/controllers/projects/settings/integrations_controller_spec.rb
index 8624eb4d1a0..93e8d03098a 100644
--- a/spec/controllers/projects/settings/integrations_controller_spec.rb
+++ b/spec/controllers/projects/settings/integrations_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Settings::IntegrationsController do
diff --git a/spec/controllers/projects/settings/operations_controller_spec.rb b/spec/controllers/projects/settings/operations_controller_spec.rb
index d989ec22481..aa9cd41ed19 100644
--- a/spec/controllers/projects/settings/operations_controller_spec.rb
+++ b/spec/controllers/projects/settings/operations_controller_spec.rb
@@ -11,137 +11,172 @@ describe Projects::Settings::OperationsController do
project.add_maintainer(user)
end
- context 'error tracking' do
- describe 'GET #show' do
- it 'renders show template' do
- get :show, params: project_params(project)
+ shared_examples 'PATCHable' do
+ let(:operations_update_service) { instance_double(::Projects::Operations::UpdateService) }
+ let(:operations_url) { project_settings_operations_url(project) }
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
- end
+ let(:permitted_params) do
+ ActionController::Parameters.new(params).permit!
+ end
- context 'with existing setting' do
- let!(:error_tracking_setting) do
- create(:project_error_tracking_setting, project: project)
- end
+ context 'format json' do
+ context 'when update succeeds' do
+ it 'returns success status' do
+ stub_operations_update_service_returning(status: :success)
- it 'loads existing setting' do
- get :show, params: project_params(project)
+ patch :update,
+ params: project_params(project, params),
+ format: :json
- expect(controller.helpers.error_tracking_setting)
- .to eq(error_tracking_setting)
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq('status' => 'success')
+ expect(flash[:notice]).to eq('Your changes have been saved')
end
end
- context 'without an existing setting' do
- it 'builds a new setting' do
- get :show, params: project_params(project)
-
- expect(controller.helpers.error_tracking_setting).to be_new_record
+ context 'when update fails' do
+ it 'returns error' do
+ stub_operations_update_service_returning(
+ status: :error,
+ message: 'error message'
+ )
+
+ patch :update,
+ params: project_params(project, params),
+ format: :json
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']).to eq('error message')
end
end
+ end
- context 'with insufficient permissions' do
- before do
- project.add_reporter(user)
- end
+ private
- it 'renders 404' do
- get :show, params: project_params(project)
+ def stub_operations_update_service_returning(return_value = {})
+ expect(::Projects::Operations::UpdateService)
+ .to receive(:new).with(project, user, permitted_params)
+ .and_return(operations_update_service)
+ expect(operations_update_service).to receive(:execute)
+ .and_return(return_value)
+ end
+ end
- expect(response).to have_gitlab_http_status(:not_found)
- end
- end
+ describe 'GET #show' do
+ it 'renders show template' do
+ get :show, params: project_params(project)
- context 'as an anonymous user' do
- before do
- sign_out(user)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to render_template(:show)
+ end
- it 'redirects to signup page' do
- get :show, params: project_params(project)
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
- expect(response).to redirect_to(new_user_session_path)
- end
+ it 'renders 404' do
+ get :show, params: project_params(project)
+
+ expect(response).to have_gitlab_http_status(:not_found)
end
end
- describe 'PATCH #update' do
- let(:operations_update_service) { spy(:operations_update_service) }
- let(:operations_url) { project_settings_operations_url(project) }
-
- let(:error_tracking_params) do
- {
- error_tracking_setting_attributes: {
- enabled: '1',
- api_url: 'http://url',
- token: 'token'
- }
- }
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
end
- let(:error_tracking_permitted) do
- ActionController::Parameters.new(error_tracking_params).permit!
+
+ it 'redirects to signup page' do
+ get :show, params: project_params(project)
+
+ expect(response).to redirect_to(new_user_session_path)
end
+ end
+ end
- context 'when update succeeds' do
- before do
- stub_operations_update_service_returning(status: :success)
- end
+ describe 'PATCH #update' do
+ context 'with insufficient permissions' do
+ before do
+ project.add_reporter(user)
+ end
- it 'shows a notice' do
- patch :update, params: project_params(project, error_tracking_params)
+ it 'renders 404' do
+ patch :update, params: project_params(project)
- expect(response).to redirect_to(operations_url)
- expect(flash[:notice]).to eq _('Your changes have been saved')
- end
+ expect(response).to have_gitlab_http_status(:not_found)
end
+ end
- context 'when update fails' do
- before do
- stub_operations_update_service_returning(status: :error)
- end
+ context 'as an anonymous user' do
+ before do
+ sign_out(user)
+ end
- it 'renders show page' do
- patch :update, params: project_params(project, error_tracking_params)
+ it 'redirects to signup page' do
+ patch :update, params: project_params(project)
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
+ end
+ end
- context 'with insufficient permissions' do
- before do
- project.add_reporter(user)
+ context 'error tracking' do
+ describe 'GET #show' do
+ context 'with existing setting' do
+ let!(:error_tracking_setting) do
+ create(:project_error_tracking_setting, project: project)
end
- it 'renders 404' do
- patch :update, params: project_params(project)
+ it 'loads existing setting' do
+ get :show, params: project_params(project)
- expect(response).to have_gitlab_http_status(:not_found)
+ expect(controller.helpers.error_tracking_setting)
+ .to eq(error_tracking_setting)
end
end
- context 'as an anonymous user' do
- before do
- sign_out(user)
- end
-
- it 'redirects to signup page' do
- patch :update, params: project_params(project)
+ context 'without an existing setting' do
+ it 'builds a new setting' do
+ get :show, params: project_params(project)
- expect(response).to redirect_to(new_user_session_path)
+ expect(controller.helpers.error_tracking_setting).to be_new_record
end
end
end
- private
+ describe 'PATCH #update' do
+ let(:params) do
+ {
+ error_tracking_setting_attributes: {
+ enabled: '1',
+ api_host: 'http://url',
+ token: 'token',
+ project: {
+ slug: 'sentry-project',
+ name: 'Sentry Project',
+ organization_slug: 'sentry-org',
+ organization_name: 'Sentry Org'
+ }
+ }
+ }
+ end
- def stub_operations_update_service_returning(return_value = {})
- expect(::Projects::Operations::UpdateService)
- .to receive(:new).with(project, user, error_tracking_permitted)
- .and_return(operations_update_service)
- expect(operations_update_service).to receive(:execute)
- .and_return(return_value)
+ it_behaves_like 'PATCHable'
+ end
+ end
+
+ context 'metrics dashboard setting' do
+ describe 'PATCH #update' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: 'https://gitlab.com'
+ }
+ }
+ end
+
+ it_behaves_like 'PATCHable'
end
end
diff --git a/spec/controllers/projects/settings/repository_controller_spec.rb b/spec/controllers/projects/settings/repository_controller_spec.rb
index 638cce60a25..b34053fc993 100644
--- a/spec/controllers/projects/settings/repository_controller_spec.rb
+++ b/spec/controllers/projects/settings/repository_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::Settings::RepositoryController do
diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb
index 8d9cb2c8ac0..9b5d7317c11 100644
--- a/spec/controllers/projects/snippets_controller_spec.rb
+++ b/spec/controllers/projects/snippets_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::SnippetsController do
diff --git a/spec/controllers/projects/stages_controller_spec.rb b/spec/controllers/projects/stages_controller_spec.rb
new file mode 100644
index 00000000000..a91e3523fd7
--- /dev/null
+++ b/spec/controllers/projects/stages_controller_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::StagesController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'POST #play_manual.json' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:stage_name) { 'test' }
+
+ before do
+ create_manual_build(pipeline, 'test', 'rspec 1/2')
+ create_manual_build(pipeline, 'test', 'rspec 2/2')
+
+ pipeline.reload
+ end
+
+ context 'when user does not have access' do
+ it 'returns not authorized' do
+ play_manual_stage!
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when user has access' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when the stage does not exists' do
+ let(:stage_name) { 'deploy' }
+
+ it 'fails to play all manual' do
+ play_manual_stage!
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when the stage exists' do
+ it 'starts all manual jobs' do
+ expect(pipeline.builds.manual.count).to eq(2)
+
+ play_manual_stage!
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(pipeline.builds.manual.count).to eq(0)
+ end
+ end
+ end
+
+ def play_manual_stage!
+ post :play_manual, params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: pipeline.id,
+ stage_name: stage_name
+ }, format: :json
+ end
+
+ def create_manual_build(pipeline, stage, name)
+ create(:ci_build, :manual, pipeline: pipeline, stage: stage, name: name)
+ end
+ end
+end
diff --git a/spec/controllers/projects/tags/releases_controller_spec.rb b/spec/controllers/projects/tags/releases_controller_spec.rb
index 29f206c574b..66eff4844c2 100644
--- a/spec/controllers/projects/tags/releases_controller_spec.rb
+++ b/spec/controllers/projects/tags/releases_controller_spec.rb
@@ -18,40 +18,85 @@ describe Projects::Tags::ReleasesController do
tag_id = release.tag
project.releases.destroy_all # rubocop: disable DestroyAll
- get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id }
+ response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: tag_id }
release = assigns(:release)
expect(release).not_to be_nil
expect(release).not_to be_persisted
+ expect(response).to have_http_status(:ok)
end
it 'retrieves an existing release' do
- get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag }
+ response = get :edit, params: { namespace_id: project.namespace, project_id: project, tag_id: release.tag }
release = assigns(:release)
expect(release).not_to be_nil
expect(release).to be_persisted
+ expect(response).to have_http_status(:ok)
end
end
describe 'PUT #update' do
it 'updates release note description' do
- update_release('description updated')
+ response = update_release(release.tag, "description updated")
- release = project.releases.find_by_tag(tag)
+ release = project.releases.find_by(tag: tag)
expect(release.description).to eq("description updated")
+ expect(response).to have_http_status(:found)
end
- it 'deletes release note when description is null' do
- expect { update_release('') }.to change(project.releases, :count).by(-1)
+ it 'creates a release if one does not exist' do
+ tag_without_release = create_new_tag
+
+ expect do
+ update_release(tag_without_release.name, "a new release")
+ end.to change { project.releases.count }.by(1)
+
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'sets the release name, sha, and author for a new release' do
+ tag_without_release = create_new_tag
+
+ response = update_release(tag_without_release.name, "a new release")
+
+ release = project.releases.find_by(tag: tag_without_release.name)
+ expect(release.name).to eq(tag_without_release.name)
+ expect(release.sha).to eq(tag_without_release.target_commit.sha)
+ expect(release.author.id).to eq(user.id)
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'deletes release when description is empty' do
+ initial_releases_count = project.releases.count
+
+ response = update_release(release.tag, "")
+
+ expect(initial_releases_count).to eq(1)
+ expect(project.releases.count).to eq(0)
+ expect(response).to have_http_status(:found)
+ end
+
+ it 'does nothing when description is empty and the tag does not have a release' do
+ tag_without_release = create_new_tag
+
+ expect do
+ update_release(tag_without_release.name, "")
+ end.not_to change { project.releases.count }
+
+ expect(response).to have_http_status(:found)
end
end
- def update_release(description)
+ def create_new_tag
+ project.repository.add_tag(user, 'mytag', 'master')
+ end
+
+ def update_release(tag_id, description)
put :update, params: {
namespace_id: project.namespace.to_param,
project_id: project,
- tag_id: release.tag,
+ tag_id: tag_id,
release: { description: description }
}
end
diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb
index 379430bff3b..b99b5d611fc 100644
--- a/spec/controllers/projects/tags_controller_spec.rb
+++ b/spec/controllers/projects/tags_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::TagsController do
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 01e53669627..bebf17728c0 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::TemplatesController do
diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index 987772f38aa..c12019fed5e 100644
--- a/spec/controllers/projects/todos_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require('spec_helper')
describe Projects::TodosController do
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index b15a2bc84a5..7f7cabe3b0c 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::TreeController do
@@ -16,6 +18,8 @@ describe Projects::TreeController do
render_views
before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+
get(:show,
params: {
namespace_id: project.namespace.to_param,
@@ -70,6 +74,26 @@ describe Projects::TreeController do
end
end
+ describe 'GET show with whitespace in ref' do
+ render_views
+
+ let(:id) { "this ref/api/responses" }
+
+ it 'does not call make a Gitaly request' do
+ allow(::Gitlab::GitalyClient).to receive(:call).and_call_original
+ expect(::Gitlab::GitalyClient).not_to receive(:call).with(anything, :commit_service, :find_commit, anything, anything)
+
+ get(:show,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: id
+ })
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
describe 'GET show with blob path' do
render_views
diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index cfa67683dd3..776c1270977 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::UploadsController do
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index 8cceda72c28..a2a09e2580f 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::VariablesController do
diff --git a/spec/controllers/projects/wikis_controller_spec.rb b/spec/controllers/projects/wikis_controller_spec.rb
index b2f40231796..f2e0b5e5c1d 100644
--- a/spec/controllers/projects/wikis_controller_spec.rb
+++ b/spec/controllers/projects/wikis_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::WikisController do
@@ -17,6 +19,18 @@ describe Projects::WikisController do
destroy_page(wiki_title)
end
+ describe 'GET #pages' do
+ subject { get :pages, params: { namespace_id: project.namespace, project_id: project, id: wiki_title } }
+
+ it 'does not load the pages content' do
+ expect(controller).to receive(:load_wiki).and_return(project_wiki)
+
+ expect(project_wiki).to receive(:list_pages).twice.and_call_original
+
+ subject
+ end
+ end
+
describe 'GET #show' do
render_views
@@ -26,9 +40,9 @@ describe Projects::WikisController do
expect(controller).to receive(:load_wiki).and_return(project_wiki)
# empty? call
- expect(project_wiki).to receive(:pages).with(limit: 1).and_call_original
+ expect(project_wiki).to receive(:list_pages).with(limit: 1).and_call_original
# Sidebar entries
- expect(project_wiki).to receive(:pages).with(limit: 15).and_call_original
+ expect(project_wiki).to receive(:list_pages).with(limit: 15).and_call_original
subject
@@ -102,7 +116,7 @@ describe Projects::WikisController do
subject
- expect(response).to redirect_to(project_wiki_path(project, project_wiki.pages.first))
+ expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first))
end
end
@@ -136,7 +150,7 @@ describe Projects::WikisController do
allow(controller).to receive(:valid_encoding?).and_return(false)
subject
- expect(response).to redirect_to(project_wiki_path(project, project_wiki.pages.first))
+ expect(response).to redirect_to(project_wiki_path(project, project_wiki.list_pages.first))
end
end
@@ -146,7 +160,7 @@ describe Projects::WikisController do
it 'updates the page' do
subject
- wiki_page = project_wiki.pages.first
+ wiki_page = project_wiki.list_pages(load_content: true).first
expect(wiki_page.title).to eq new_title
expect(wiki_page.content).to eq new_content
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index a1662658ade..8d2412f97ef 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
require('spec_helper')
describe ProjectsController do
+ include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
let(:project) { create(:project) }
@@ -77,6 +80,10 @@ describe ProjectsController do
end
context "user has access to project" do
+ before do
+ expect(::Gitlab::GitalyClient).to receive(:allow_ref_name_caching).and_call_original
+ end
+
context "and does not have notification setting" do
it "initializes notification as disabled" do
get :show, params: { namespace_id: public_project.namespace, id: public_project }
@@ -285,6 +292,18 @@ describe ProjectsController do
end
describe 'GET edit' do
+ it 'allows an admin user to access the page' do
+ sign_in(create(:user, :admin))
+
+ get :edit,
+ params: {
+ namespace_id: project.namespace.path,
+ id: project.path
+ }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
it 'sets the badge API endpoint' do
sign_in(user)
project.add_maintainer(user)
@@ -369,6 +388,23 @@ describe ProjectsController do
end
end
+ it 'does not update namespace' do
+ controller.instance_variable_set(:@project, project)
+
+ params = {
+ namespace_id: 'test'
+ }
+
+ expect do
+ put :update,
+ params: {
+ namespace_id: project.namespace,
+ id: project.id,
+ project: params
+ }
+ end.not_to change { project.namespace.reload }
+ end
+
def update_project(**parameters)
put :update,
params: {
@@ -390,6 +426,37 @@ describe ProjectsController do
it_behaves_like 'updating a project'
end
+
+ context 'as maintainer' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it_behaves_like 'unauthorized when external service denies access' do
+ subject do
+ put :update,
+ params: {
+ namespace_id: project.namespace,
+ id: project,
+ project: { description: 'Hello world' }
+ }
+ project.reload
+ end
+
+ it 'updates when the service allows access' do
+ external_service_allow_access(user, project)
+
+ expect { subject }.to change(project, :description)
+ end
+
+ it 'does not update when the service rejects access' do
+ external_service_deny_access(user, project)
+
+ expect { subject }.not_to change(project, :description)
+ end
+ end
+ end
end
describe '#transfer' do
@@ -686,6 +753,16 @@ describe ProjectsController do
expect(JSON.parse(response.body).keys).to match_array(%w(body references))
end
+ context 'when not authorized' do
+ let(:private_project) { create(:project, :private) }
+
+ it 'returns 404' do
+ post :preview_markdown, params: { namespace_id: private_project.namespace, id: private_project, text: '*Markdown* text' }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
context 'state filter on references' do
let(:issue) { create(:issue, :closed, project: public_project) }
let(:merge_request) { create(:merge_request, :closed, target_project: public_project) }
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index fd151e8a298..9a598790ff2 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RegistrationsController do
@@ -15,7 +17,7 @@ describe RegistrationsController do
context 'when send_user_confirmation_email is false' do
it 'signs the user in' do
- allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(false)
+ stub_application_setting(send_user_confirmation_email: false)
expect { post(:create, params: user_params) }.not_to change { ActionMailer::Base.deliveries.size }
expect(subject.current_user).not_to be_nil
@@ -24,7 +26,7 @@ describe RegistrationsController do
context 'when send_user_confirmation_email is true' do
it 'does not authenticate user and sends confirmation email' do
- allow_any_instance_of(ApplicationSetting).to receive(:send_user_confirmation_email).and_return(true)
+ stub_application_setting(send_user_confirmation_email: true)
post(:create, params: user_params)
@@ -35,7 +37,7 @@ describe RegistrationsController do
context 'when signup_enabled? is false' do
it 'redirects to sign_in' do
- allow_any_instance_of(ApplicationSetting).to receive(:signup_enabled?).and_return(false)
+ stub_application_setting(signup_enabled: false)
expect { post(:create, params: user_params) }.not_to change(User, :count)
expect(response).to redirect_to(new_user_session_path)
@@ -44,13 +46,17 @@ describe RegistrationsController do
end
context 'when reCAPTCHA is enabled' do
+ def fail_recaptcha
+ # Without this, `verify_recaptcha` arbitrarily returns true in test env
+ Recaptcha.configuration.skip_verify_env.delete('test')
+ end
+
before do
stub_application_setting(recaptcha_enabled: true)
end
it 'displays an error when the reCAPTCHA is not solved' do
- # Without this, `verify_recaptcha` arbitrarily returns true in test env
- Recaptcha.configuration.skip_verify_env.delete('test')
+ fail_recaptcha
post(:create, params: user_params)
@@ -68,6 +74,17 @@ describe RegistrationsController do
expect(flash[:notice]).to include 'Welcome! You have signed up successfully.'
end
+
+ it 'does not require reCAPTCHA if disabled by feature flag' do
+ stub_feature_flags(registrations_recaptcha: false)
+ fail_recaptcha
+
+ post(:create, params: user_params)
+
+ expect(controller).not_to receive(:verify_recaptcha)
+ expect(flash[:alert]).to be_nil
+ expect(flash[:notice]).to include 'Welcome! You have signed up successfully.'
+ end
end
context 'when terms are enforced' do
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index 995f803d757..4892ff43086 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RootController do
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
index 02a0cfe0272..4634d1d4bb3 100644
--- a/spec/controllers/search_controller_spec.rb
+++ b/spec/controllers/search_controller_spec.rb
@@ -1,12 +1,40 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SearchController do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
before do
sign_in(user)
end
+ context 'uses the right partials depending on scope' do
+ using RSpec::Parameterized::TableSyntax
+ render_views
+
+ set(:project) { create(:project, :public, :repository, :wiki_repo) }
+
+ subject { get(:show, params: { project_id: project.id, scope: scope, search: 'merge' }) }
+
+ where(:partial, :scope) do
+ '_blob' | :blobs
+ '_wiki_blob' | :wiki_blobs
+ '_commit' | :commits
+ end
+
+ with_them do
+ it do
+ project_wiki = create(:project_wiki, project: project, user: user)
+ create(:wiki_page, wiki: project_wiki, attrs: { title: 'merge', content: 'merge' })
+
+ expect(subject).to render_template("search/results/#{partial}")
+ end
+ end
+ end
+
it 'finds issue comments' do
project = create(:project, :public)
note = create(:note_on_issue, project: project)
@@ -76,4 +104,41 @@ describe SearchController do
expect(assigns[:search_objects].count).to eq(0)
end
end
+
+ context 'with external authorization service enabled' do
+ let(:project) { create(:project, namespace: user.namespace) }
+ let(:note) { create(:note_on_issue, project: project) }
+
+ before do
+ enable_external_authorization_service_check
+ end
+
+ describe 'GET #show' do
+ it 'renders a 403 when no project is given' do
+ get :show, params: { scope: 'notes', search: note.note }
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'renders a 200 when a project was set' do
+ get :show, params: { project_id: project.id, scope: 'notes', search: note.note }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ describe 'GET #autocomplete' do
+ it 'renders a 403 when no project is given' do
+ get :autocomplete, params: { term: 'hello' }
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'renders a 200 when a project was set' do
+ get :autocomplete, params: { project_id: project.id, term: 'hello' }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+ end
end
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 75c91dd8607..89857a9d21b 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -1,16 +1,34 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe SentNotificationsController do
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:sent_notification) { create(:sent_notification, project: project, noteable: issue, recipient: user) }
+ let(:project) { create(:project, :public) }
+ let(:private_project) { create(:project, :private) }
+ let(:sent_notification) { create(:sent_notification, project: target_project, noteable: noteable, recipient: user) }
let(:issue) do
- create(:issue, project: project, author: user) do |issue|
- issue.subscriptions.create(user: user, project: project, subscribed: true)
+ create(:issue, project: target_project) do |issue|
+ issue.subscriptions.create(user: user, project: target_project, subscribed: true)
+ end
+ end
+
+ let(:confidential_issue) do
+ create(:issue, project: target_project, confidential: true) do |issue|
+ issue.subscriptions.create(user: user, project: target_project, subscribed: true)
end
end
+ let(:merge_request) do
+ create(:merge_request, source_project: target_project, target_project: target_project) do |mr|
+ mr.subscriptions.create(user: user, project: target_project, subscribed: true)
+ end
+ end
+
+ let(:noteable) { issue }
+ let(:target_project) { project }
+
describe 'GET unsubscribe' do
context 'when the user is not logged in' do
context 'when the force param is passed' do
@@ -32,20 +50,93 @@ describe SentNotificationsController do
end
context 'when the force param is not passed' do
+ render_views
+
before do
get(:unsubscribe, params: { id: sent_notification.reply_key })
end
- it 'does not unsubscribe the user' do
- expect(issue.subscribed?(user, project)).to be_truthy
+ shared_examples 'unsubscribing as anonymous' do
+ it 'does not unsubscribe the user' do
+ expect(noteable.subscribed?(user, target_project)).to be_truthy
+ end
+
+ it 'does not set the flash message' do
+ expect(controller).not_to set_flash[:notice]
+ end
+
+ it 'renders unsubscribe page' do
+ expect(response.status).to eq(200)
+ expect(response).to render_template :unsubscribe
+ end
end
- it 'does not set the flash message' do
- expect(controller).not_to set_flash[:notice]
+ context 'when project is public' do
+ context 'when unsubscribing from issue' do
+ let(:noteable) { issue }
+
+ it 'shows issue title' do
+ expect(response.body).to include(issue.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from confidential issue' do
+ let(:noteable) { confidential_issue }
+
+ it 'does not show issue title' do
+ expect(response.body).not_to include(confidential_issue.title)
+ expect(response.body).to include(confidential_issue.to_reference)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from merge request' do
+ let(:noteable) { merge_request }
+
+ it 'shows merge request title' do
+ expect(response.body).to include(merge_request.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
end
- it 'redirects to the login page' do
- expect(response).to render_template :unsubscribe
+ context 'when project is not public' do
+ let(:target_project) { private_project }
+
+ context 'when unsubscribing from issue' do
+ let(:noteable) { issue }
+
+ it 'shows issue title' do
+ expect(response.body).not_to include(issue.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from confidential issue' do
+ let(:noteable) { confidential_issue }
+
+ it 'does not show issue title' do
+ expect(response.body).not_to include(confidential_issue.title)
+ expect(response.body).to include(confidential_issue.to_reference)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
+
+ context 'when unsubscribing from merge request' do
+ let(:noteable) { merge_request }
+
+ it 'shows merge request title' do
+ expect(response.body).not_to include(merge_request.title)
+ end
+
+ it_behaves_like 'unsubscribing as anonymous'
+ end
end
end
end
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index ea7242c1aa8..9c4ddce5409 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SessionsController do
@@ -56,7 +58,26 @@ describe SessionsController do
it 'authenticates user correctly' do
post(:create, params: { user: user_params })
- expect(subject.current_user). to eq user
+ expect(subject.current_user).to eq user
+ end
+
+ context 'with password authentication disabled' do
+ before do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ end
+
+ it 'does not sign in the user' do
+ post(:create, params: { user: user_params })
+
+ expect(@request.env['warden']).not_to be_authenticated
+ expect(subject.current_user).to be_nil
+ end
+
+ it 'returns status 403' do
+ post(:create, params: { user: user_params })
+
+ expect(response.status).to eq 403
+ end
end
it 'creates an audit log record' do
@@ -151,6 +172,19 @@ describe SessionsController do
end
end
+ context 'with password authentication disabled' do
+ before do
+ stub_application_setting(password_authentication_enabled_for_web: false)
+ end
+
+ it 'allows 2FA stage of non-password login' do
+ authenticate_2fa(otp_attempt: user.current_otp)
+
+ expect(@request.env['warden']).to be_authenticated
+ expect(subject.current_user).to eq user
+ end
+ end
+
##
# See #14900 issue
#
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
index 6efbd1f6c71..936d7c7dae4 100644
--- a/spec/controllers/snippets/notes_controller_spec.rb
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Snippets::NotesController do
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 5c6858dc7b2..f8666a1986f 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SnippetsController do
@@ -205,6 +207,8 @@ describe SnippetsController do
end
context 'when the snippet description contains a file' do
+ include FileMoverHelpers
+
let(:picture_file) { '/-/system/temp/secret56/picture.jpg' }
let(:text_file) { '/-/system/temp/secret78/text.txt' }
let(:description) do
@@ -215,6 +219,8 @@ describe SnippetsController do
before do
allow(FileUtils).to receive(:mkdir_p)
allow(FileUtils).to receive(:move)
+ stub_file_mover(text_file)
+ stub_file_mover(picture_file)
end
subject { create_snippet({ description: description }, { files: [picture_file, text_file] }) }
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index e52a5fe42f2..d27658e02cb 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
shared_examples 'content not cached without revalidation' do
it 'ensures content will not be cached without revalidation' do
diff --git a/spec/controllers/user_callouts_controller_spec.rb b/spec/controllers/user_callouts_controller_spec.rb
index c71d75a3e7f..babc93a83e5 100644
--- a/spec/controllers/user_callouts_controller_spec.rb
+++ b/spec/controllers/user_callouts_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UserCalloutsController do
@@ -14,11 +16,11 @@ describe UserCalloutsController do
let(:feature_name) { UserCallout.feature_names.keys.first }
context 'when callout entry does not exist' do
- it 'should create a callout entry with dismissed state' do
+ it 'creates a callout entry with dismissed state' do
expect { subject }.to change { UserCallout.count }.by(1)
end
- it 'should return success' do
+ it 'returns success' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -28,7 +30,7 @@ describe UserCalloutsController do
context 'when callout entry already exists' do
let!(:callout) { create(:user_callout, feature_name: UserCallout.feature_names.keys.first, user: user) }
- it 'should return success' do
+ it 'returns success' do
subject
expect(response).to have_gitlab_http_status(:ok)
@@ -39,7 +41,7 @@ describe UserCalloutsController do
context 'with invalid feature name' do
let(:feature_name) { 'bogus_feature_name' }
- it 'should return bad request' do
+ it 'returns bad request' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
diff --git a/spec/controllers/users/terms_controller_spec.rb b/spec/controllers/users/terms_controller_spec.rb
index cbfd2b17864..e0bdec3df1d 100644
--- a/spec/controllers/users/terms_controller_spec.rb
+++ b/spec/controllers/users/terms_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::TermsController do
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index af61026098b..c3d6ea9cbcd 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UsersController do
@@ -185,13 +187,32 @@ describe UsersController do
context 'for user' do
context 'with public profile' do
- it 'renders calendar_activities' do
- push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user)
- EventCreateService.new.push(project, public_user, push_data)
+ let(:issue) { create(:issue, project: project, author: user) }
+ let(:note) { create(:note, noteable: issue, author: user, project: project) }
+ render_views
+
+ before do
+ create_push_event
+ create_note_event
+ end
+
+ it 'renders calendar_activities' do
get :calendar_activities, params: { username: public_user.username }
+
expect(assigns[:events]).not_to be_empty
end
+
+ it 'avoids N+1 queries', :request_store do
+ get :calendar_activities, params: { username: public_user.username }
+
+ control = ActiveRecord::QueryRecorder.new { get :calendar_activities, params: { username: public_user.username } }
+
+ create_push_event
+ create_note_event
+
+ expect { get :calendar_activities, params: { username: public_user.username } }.not_to exceed_query_limit(control)
+ end
end
context 'with private profile' do
@@ -203,6 +224,21 @@ describe UsersController do
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ context 'external authorization' do
+ subject { get :calendar_activities, params: { username: user.username } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
+
+ def create_push_event
+ push_data = Gitlab::DataBuilder::Push.build_sample(project, public_user)
+ EventCreateService.new.push(project, public_user, push_data)
+ end
+
+ def create_note_event
+ EventCreateService.new.leave_note(note, public_user)
+ end
end
end
@@ -258,6 +294,12 @@ describe UsersController do
expect(JSON.parse(response.body)).to have_key('html')
end
end
+
+ context 'external authorization' do
+ subject { get :snippets, params: { username: user.username } }
+
+ it_behaves_like 'disabled when using an external authorization service'
+ end
end
describe 'GET #exists' do
diff --git a/spec/db/importers/common_metrics_importer_spec.rb b/spec/db/importers/common_metrics_importer_spec.rb
index 6133b17ac61..a717c8cd04d 100644
--- a/spec/db/importers/common_metrics_importer_spec.rb
+++ b/spec/db/importers/common_metrics_importer_spec.rb
@@ -23,10 +23,10 @@ describe Importers::CommonMetricsImporter do
subject { described_class.new }
context "does import common_metrics.yml" do
- let(:groups) { subject.content }
- let(:metrics) { groups.map { |group| group['metrics'] }.flatten }
- let(:queries) { metrics.map { |group| group['queries'] }.flatten }
- let(:query_ids) { queries.map { |query| query['id'] } }
+ let(:groups) { subject.content['panel_groups'] }
+ let(:panels) { groups.map { |group| group['panels'] }.flatten }
+ let(:metrics) { panels.map { |group| group['metrics'] }.flatten }
+ let(:metric_ids) { metrics.map { |metric| metric['id'] } }
before do
subject.execute
@@ -36,20 +36,20 @@ describe Importers::CommonMetricsImporter do
expect(PrometheusMetric.common.group(:group).count.count).to eq(groups.count)
end
- it "has the same amount of metrics" do
- expect(PrometheusMetric.common.group(:group, :title).count.count).to eq(metrics.count)
+ it "has the same amount of panels" do
+ expect(PrometheusMetric.common.group(:group, :title).count.count).to eq(panels.count)
end
- it "has the same amount of queries" do
- expect(PrometheusMetric.common.count).to eq(queries.count)
+ it "has the same amount of metrics" do
+ expect(PrometheusMetric.common.count).to eq(metrics.count)
end
it "does not have duplicate IDs" do
- expect(query_ids).to eq(query_ids.uniq)
+ expect(metric_ids).to eq(metric_ids.uniq)
end
it "imports all IDs" do
- expect(PrometheusMetric.common.pluck(:identifier)).to contain_exactly(*query_ids)
+ expect(PrometheusMetric.common.pluck(:identifier)).to contain_exactly(*metric_ids)
end
end
@@ -65,24 +65,26 @@ describe Importers::CommonMetricsImporter do
context 'does import properly all fields' do
let(:query_identifier) { 'response-metric' }
- let(:group) do
+ let(:dashboard) do
{
- group: 'Response metrics (NGINX Ingress)',
- metrics: [{
- title: "Throughput",
- y_label: "Requests / Sec",
- queries: [{
- id: query_identifier,
- query_range: 'my-query',
- unit: 'my-unit',
- label: 'status code'
+ panel_groups: [{
+ group: 'Response metrics (NGINX Ingress)',
+ panels: [{
+ title: "Throughput",
+ y_label: "Requests / Sec",
+ metrics: [{
+ id: query_identifier,
+ query_range: 'my-query',
+ unit: 'my-unit',
+ label: 'status code'
+ }]
}]
}]
}
end
before do
- expect(subject).to receive(:content) { [group.deep_stringify_keys] }
+ expect(subject).to receive(:content) { dashboard.deep_stringify_keys }
end
shared_examples 'stores metric' do
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb
index 897b4411055..40c3a6d90d0 100644
--- a/spec/db/schema_spec.rb
+++ b/spec/db/schema_spec.rb
@@ -25,12 +25,12 @@ describe 'Database schema' do
events: %w[target_id],
forked_project_links: %w[forked_from_project_id],
identities: %w[user_id],
- issues: %w[last_edited_by_id],
+ issues: %w[last_edited_by_id state_id],
keys: %w[user_id],
label_links: %w[target_id],
lfs_objects_projects: %w[lfs_object_id project_id],
members: %w[source_id created_by_id],
- merge_requests: %w[last_edited_by_id],
+ merge_requests: %w[last_edited_by_id state_id],
namespaces: %w[owner_id parent_id],
notes: %w[author_id commit_id noteable_id updated_by_id resolved_by_id discussion_id],
notification_settings: %w[source_id],
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 0b3e67b4987..a473136b57b 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -75,6 +75,10 @@ FactoryBot.define do
status 'created'
end
+ trait :preparing do
+ status 'preparing'
+ end
+
trait :scheduled do
schedulable
status 'scheduled'
@@ -244,17 +248,6 @@ FactoryBot.define do
runner factory: :ci_runner
end
- trait :legacy_artifacts do
- after(:create) do |build, _|
- build.update!(
- legacy_artifacts_file: fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip'),
- legacy_artifacts_metadata: fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip')
- )
- end
- end
-
trait :artifacts do
after(:create) do |build|
create(:ci_job_artifact, :archive, job: build, expire_at: build.artifacts_expire_at)
@@ -332,6 +325,11 @@ FactoryBot.define do
failure_reason 2
end
+ trait :prerequisite_failure do
+ failed
+ failure_reason 10
+ end
+
trait :with_runner_session do
after(:build) do |build|
build.build_runner_session(url: 'https://localhost')
diff --git a/spec/factories/ci/group_variables.rb b/spec/factories/ci/group_variables.rb
index 64716842b12..9bf520a2c0a 100644
--- a/spec/factories/ci/group_variables.rb
+++ b/spec/factories/ci/group_variables.rb
@@ -2,6 +2,7 @@ FactoryBot.define do
factory :ci_group_variable, class: Ci::GroupVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
+ masked false
trait(:protected) do
protected true
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 2c76c22ba69..542fa9775cd 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -45,9 +45,12 @@ FactoryBot.define do
file_type :archive
file_format :zip
- after(:build) do |artifact, _|
- artifact.file = fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip')
+ transient do
+ file { fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') }
+ end
+
+ after(:build) do |artifact, evaluator|
+ artifact.file = evaluator.file
end
end
@@ -61,9 +64,12 @@ FactoryBot.define do
file_type :metadata
file_format :gzip
- after(:build) do |artifact, _|
- artifact.file = fixture_file_upload(
- Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip')
+ transient do
+ file { fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), 'application/x-gzip') }
+ end
+
+ after(:build) do |artifact, evaluator|
+ artifact.file = evaluator.file
end
end
diff --git a/spec/factories/ci/pipeline_schedule.rb b/spec/factories/ci/pipeline_schedule.rb
index b2b79807429..4b83ba2ac1b 100644
--- a/spec/factories/ci/pipeline_schedule.rb
+++ b/spec/factories/ci/pipeline_schedule.rb
@@ -7,6 +7,16 @@ FactoryBot.define do
description "pipeline schedule"
project
+ trait :every_minute do
+ cron '*/1 * * * *'
+ cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ end
+
+ trait :hourly do
+ cron '* */1 * * *'
+ cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
+ end
+
trait :nightly do
cron '0 1 * * *'
cron_timezone Gitlab::Ci::CronParser::VALID_SYNTAX_SAMPLE_TIME_ZONE
diff --git a/spec/factories/ci/pipeline_schedule_variables.rb b/spec/factories/ci/pipeline_schedule_variables.rb
index 8d29118e310..c85b97fbfc7 100644
--- a/spec/factories/ci/pipeline_schedule_variables.rb
+++ b/spec/factories/ci/pipeline_schedule_variables.rb
@@ -2,6 +2,7 @@ FactoryBot.define do
factory :ci_pipeline_schedule_variable, class: Ci::PipelineScheduleVariable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
+ variable_type 'env_var'
pipeline_schedule factory: :ci_pipeline_schedule
end
diff --git a/spec/factories/ci/pipelines.rb b/spec/factories/ci/pipelines.rb
index 8a44ce52849..aa5ccbda6cd 100644
--- a/spec/factories/ci/pipelines.rb
+++ b/spec/factories/ci/pipelines.rb
@@ -50,6 +50,14 @@ FactoryBot.define do
failure_reason :config_error
end
+ trait :created do
+ status :created
+ end
+
+ trait :preparing do
+ status :preparing
+ end
+
trait :blocked do
status :manual
end
@@ -82,6 +90,12 @@ FactoryBot.define do
end
end
+ trait :with_job do
+ after(:build) do |pipeline, evaluator|
+ pipeline.builds << build(:ci_build, pipeline: pipeline, project: pipeline.project)
+ end
+ end
+
trait :auto_devops_source do
config_source { Ci::Pipeline.config_sources[:auto_devops_source] }
end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index 3d014b9b54f..97a7c9ba252 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -2,6 +2,7 @@ FactoryBot.define do
factory :ci_variable, class: Ci::Variable do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
+ masked false
trait(:protected) do
protected true
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index fe56ac5b71d..d78f01828d7 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -6,6 +6,11 @@ FactoryBot.define do
status(-2)
end
+ trait :errored do
+ status(-1)
+ status_reason 'something went wrong'
+ end
+
trait :installable do
status 0
end
@@ -30,17 +35,21 @@ FactoryBot.define do
status 5
end
- trait :errored do
- status(-1)
+ trait :update_errored do
+ status(6)
status_reason 'something went wrong'
end
- trait :update_errored do
- status(6)
+ trait :uninstalling do
+ status 7
+ end
+
+ trait :uninstall_errored do
+ status(8)
status_reason 'something went wrong'
end
- trait :timeouted do
+ trait :timed_out do
installing
updated_at { ClusterWaitForAppInstallationWorker::TIMEOUT.ago }
end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index a2e5f4862db..6eb0194b710 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -3,6 +3,7 @@ FactoryBot.define do
user
name 'test-cluster'
cluster_type :project_type
+ managed true
trait :instance do
cluster_type { Clusters::Cluster.cluster_types[:instance_type] }
@@ -12,7 +13,7 @@ FactoryBot.define do
cluster_type { Clusters::Cluster.cluster_types[:project_type] }
before(:create) do |cluster, evaluator|
- cluster.projects << create(:project, :repository)
+ cluster.projects << create(:project, :repository) unless cluster.projects.present?
end
end
@@ -20,7 +21,7 @@ FactoryBot.define do
cluster_type { Clusters::Cluster.cluster_types[:group_type] }
before(:create) do |cluster, evalutor|
- cluster.groups << create(:group)
+ cluster.groups << create(:group) unless cluster.groups.present?
end
end
@@ -63,5 +64,9 @@ FactoryBot.define do
trait :with_domain do
domain 'example.com'
end
+
+ trait :not_managed do
+ managed false
+ end
end
end
diff --git a/spec/factories/clusters/providers/gcp.rb b/spec/factories/clusters/providers/gcp.rb
index a002ab28519..186c7c8027c 100644
--- a/spec/factories/clusters/providers/gcp.rb
+++ b/spec/factories/clusters/providers/gcp.rb
@@ -28,5 +28,9 @@ FactoryBot.define do
gcp.make_errored('Something wrong')
end
end
+
+ trait :abac_enabled do
+ legacy_abac true
+ end
end
end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 381bf07f6a0..848a31e96c1 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -33,6 +33,10 @@ FactoryBot.define do
status 'pending'
end
+ trait :preparing do
+ status 'preparing'
+ end
+
trait :created do
status 'created'
end
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 011c98599a3..db438ad32d3 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -1,6 +1,6 @@
FactoryBot.define do
factory :deployment, class: Deployment do
- sha '97de212e80737a608d939f648d959671fb0a0142'
+ sha 'b83d6e391c22777fca1ed3012fce84f633d7fed0'
ref 'master'
tag false
user nil
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 3b354c0d96b..18a0c2ec731 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -4,6 +4,7 @@ FactoryBot.define do
path { name.downcase.gsub(/\s/, '_') }
type 'Group'
owner nil
+ project_creation_level ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS
after(:create) do |group|
if group.owner
@@ -36,5 +37,13 @@ FactoryBot.define do
trait :nested do
parent factory: :group
end
+
+ trait :auto_devops_enabled do
+ auto_devops_enabled true
+ end
+
+ trait :auto_devops_disabled do
+ auto_devops_enabled false
+ end
end
end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 2392bfc4a53..0b6a43b13a9 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -46,10 +46,26 @@ FactoryBot.define do
target_branch "improve/awesome"
end
+ trait :merged_last_month do
+ merged
+
+ after(:build) do |merge_request|
+ merge_request.build_metrics.merged_at = 1.month.ago
+ end
+ end
+
trait :closed do
state :closed
end
+ trait :closed_last_month do
+ closed
+
+ after(:build) do |merge_request|
+ merge_request.build_metrics.latest_closed_at = 1.month.ago
+ end
+ end
+
trait :opened do
state :opened
end
@@ -79,7 +95,8 @@ FactoryBot.define do
end
trait :merge_when_pipeline_succeeds do
- merge_when_pipeline_succeeds true
+ auto_merge_enabled true
+ auto_merge_strategy AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS
merge_user { author }
end
@@ -101,6 +118,47 @@ FactoryBot.define do
end
end
+ trait :with_legacy_detached_merge_request_pipeline do
+ after(:create) do |merge_request|
+ merge_request.pipelines_for_merge_request << create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request,
+ project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.source_branch_sha)
+ end
+ end
+
+ trait :with_detached_merge_request_pipeline do
+ after(:create) do |merge_request|
+ merge_request.pipelines_for_merge_request << create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request,
+ project: merge_request.source_project,
+ ref: merge_request.ref_path,
+ sha: merge_request.source_branch_sha)
+ end
+ end
+
+ trait :with_merge_request_pipeline do
+ transient do
+ merge_sha { 'test-merge-sha' }
+ source_sha { source_branch_sha }
+ target_sha { target_branch_sha }
+ end
+
+ after(:create) do |merge_request, evaluator|
+ merge_request.pipelines_for_merge_request << create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request,
+ project: merge_request.source_project,
+ ref: merge_request.merge_ref_path,
+ sha: evaluator.merge_sha,
+ source_sha: evaluator.source_sha,
+ target_sha: evaluator.target_sha)
+ end
+ end
+
trait :deployed_review_app do
target_branch 'pages-deploy-target'
diff --git a/spec/factories/pages_domain_acme_orders.rb b/spec/factories/pages_domain_acme_orders.rb
new file mode 100644
index 00000000000..7f9ee1c8f9c
--- /dev/null
+++ b/spec/factories/pages_domain_acme_orders.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :pages_domain_acme_order do
+ pages_domain
+ url { 'https://example.com/' }
+ expires_at { 1.day.from_now }
+ challenge_token { 'challenge_token' }
+ challenge_file_content { 'filecontent' }
+
+ private_key { OpenSSL::PKey::RSA.new(4096).to_pem }
+
+ trait :expired do
+ expires_at { 1.day.ago }
+ end
+ end
+end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index 20671da016e..db8384877b0 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -41,6 +41,14 @@ nNp/xedE1YxutQ==
enabled_until nil
end
+ trait :scheduled_for_removal do
+ remove_at { 1.day.from_now }
+ end
+
+ trait :should_be_removed do
+ remove_at { 1.day.ago }
+ end
+
trait :unverified do
verified_at nil
end
diff --git a/spec/factories/pool_repositories.rb b/spec/factories/pool_repositories.rb
index 36e54cf44b4..8cac666069c 100644
--- a/spec/factories/pool_repositories.rb
+++ b/spec/factories/pool_repositories.rb
@@ -5,6 +5,7 @@ FactoryBot.define do
before(:create) do |pool|
pool.source_project = create(:project, :repository)
+ pool.source_project.update!(pool_repository: pool)
end
trait :scheduled do
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
index 75ac7cc7687..1de42512402 100644
--- a/spec/factories/project_auto_devops.rb
+++ b/spec/factories/project_auto_devops.rb
@@ -2,7 +2,6 @@ FactoryBot.define do
factory :project_auto_devops do
project
enabled true
- domain "example.com"
deploy_strategy :continuous
trait :continuous_deployment do
diff --git a/spec/factories/project_daily_statistics.rb b/spec/factories/project_daily_statistics.rb
new file mode 100644
index 00000000000..7e4142fa401
--- /dev/null
+++ b/spec/factories/project_daily_statistics.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_daily_statistic do
+ project
+ fetch_count 1
+ end
+end
diff --git a/spec/factories/project_metrics_settings.rb b/spec/factories/project_metrics_settings.rb
new file mode 100644
index 00000000000..234753f9b87
--- /dev/null
+++ b/spec/factories/project_metrics_settings.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :project_metrics_setting, class: ProjectMetricsSetting do
+ project
+ external_dashboard_url 'https://grafana.com'
+ end
+end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index f7ef34d773b..743ec322885 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -260,6 +260,7 @@ FactoryBot.define do
trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED }
trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED }
trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE }
+ trait(:merge_requests_public) { merge_requests_access_level ProjectFeature::PUBLIC }
trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
@@ -271,6 +272,10 @@ FactoryBot.define do
trait :auto_devops do
association :auto_devops, factory: :project_auto_devops
end
+
+ trait :auto_devops_disabled do
+ association :auto_devops, factory: [:project_auto_devops, :disabled]
+ end
end
# Project with empty repository
@@ -313,6 +318,20 @@ FactoryBot.define do
end
end
+ factory :youtrack_project, parent: :project do
+ has_external_issue_tracker true
+
+ after :create do |project|
+ project.create_youtrack_service(
+ active: true,
+ properties: {
+ 'project_url' => 'http://youtrack/projects/project_guid_in_youtrack',
+ 'issues_url' => 'http://youtrack/issues/:id'
+ }
+ )
+ end
+ end
+
factory :jira_project, parent: :project do
has_external_issue_tracker true
jira_service
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 70c34f8640b..0d8c26a2ee9 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -62,4 +62,10 @@ FactoryBot.define do
project_key: 'jira-key'
)
end
+
+ factory :hipchat_service do
+ project
+ type 'HipchatService'
+ token 'test_token'
+ end
end
diff --git a/spec/factories/suggestions.rb b/spec/factories/suggestions.rb
index 307523cc061..b1427e0211f 100644
--- a/spec/factories/suggestions.rb
+++ b/spec/factories/suggestions.rb
@@ -16,5 +16,11 @@ FactoryBot.define do
applied true
commit_id { RepoHelpers.sample_commit.id }
end
+
+ trait :content_from_repo do
+ after(:build) do |suggestion, evaluator|
+ suggestion.from_content = suggestion.fetch_from_content
+ end
+ end
end
end
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index 7256f785e1f..52f6962f16b 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -13,7 +13,7 @@ FactoryBot.define do
end
# this needs to comply with RecordsUpload::Concern#upload_path
- path { File.join("uploads/-/system", model.class.to_s.underscore, mount_point.to_s, 'avatar.jpg') }
+ path { File.join("uploads/-/system", model.class.underscore, mount_point.to_s, 'avatar.jpg') }
trait :personal_snippet_upload do
uploader "PersonalFileUploader"
@@ -54,10 +54,7 @@ FactoryBot.define do
end
trait :attachment_upload do
- transient do
- mount_point :attachment
- end
-
+ mount_point :attachment
model { build(:note) }
uploader "AttachmentUploader"
end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index 83cd686818c..f6c498f7a4c 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Admin Appearance' do
diff --git a/spec/features/admin/admin_browses_logs_spec.rb b/spec/features/admin/admin_browses_logs_spec.rb
index 02f50d7e27f..1f83d04d9aa 100644
--- a/spec/features/admin/admin_browses_logs_spec.rb
+++ b/spec/features/admin/admin_browses_logs_spec.rb
@@ -13,5 +13,6 @@ describe 'Admin browses logs' do
expect(page).to have_link 'test.log'
expect(page).to have_link 'sidekiq.log'
expect(page).to have_link 'repocheck.log'
+ expect(page).to have_link 'kubernetes.log'
end
end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index 25ed3bdc88e..ce780789f5a 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -73,22 +73,26 @@ describe 'Admin::Hooks' do
end
describe 'Remove existing hook', :js do
+ let(:hook_url) { generate(:url) }
+
before do
- create(:system_hook)
+ create(:system_hook, url: hook_url)
end
context 'removes existing hook' do
it 'from hooks list page' do
visit admin_hooks_path
- expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1)
+ accept_confirm { click_link 'Remove' }
+ expect(page).not_to have_content(hook_url)
end
it 'from hook edit page' do
visit admin_hooks_path
click_link 'Edit'
- expect { accept_confirm { find(:link, 'Remove').send_keys(:return) } }.to change(SystemHook, :count).by(-1)
+ accept_confirm { click_link 'Remove' }
+ expect(page).not_to have_content(hook_url)
end
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index ed9c0ea9ac0..97b432a6751 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -141,6 +141,56 @@ describe "Admin Runners" do
end
end
+ describe 'filter by tag', :js do
+ it 'shows correct runner when tag matches' do
+ create :ci_runner, description: 'runner-blue', tag_list: ['blue']
+ create :ci_runner, description: 'runner-red', tag_list: ['red']
+
+ visit admin_runners_path
+
+ expect(page).to have_content 'runner-blue'
+ expect(page).to have_content 'runner-red'
+
+ input_filtered_search_keys('tag:blue')
+
+ expect(page).to have_content 'runner-blue'
+ expect(page).not_to have_content 'runner-red'
+ end
+
+ it 'shows no runner when tag does not match' do
+ create :ci_runner, description: 'runner-blue', tag_list: ['blue']
+ create :ci_runner, description: 'runner-red', tag_list: ['blue']
+
+ visit admin_runners_path
+
+ input_filtered_search_keys('tag:red')
+
+ expect(page).not_to have_content 'runner-blue'
+ expect(page).not_to have_content 'runner-blue'
+ expect(page).to have_text 'No runners found'
+ end
+
+ it 'shows correct runner when tag is selected and search term is entered' do
+ create :ci_runner, description: 'runner-a-1', tag_list: ['blue']
+ create :ci_runner, description: 'runner-a-2', tag_list: ['red']
+ create :ci_runner, description: 'runner-b-1', tag_list: ['blue']
+
+ visit admin_runners_path
+
+ input_filtered_search_keys('tag:blue')
+
+ expect(page).to have_content 'runner-a-1'
+ expect(page).to have_content 'runner-b-1'
+ expect(page).not_to have_content 'runner-a-2'
+
+ input_filtered_search_keys('tag:blue runner-a')
+
+ expect(page).to have_content 'runner-a-1'
+ expect(page).not_to have_content 'runner-b-1'
+ expect(page).not_to have_content 'runner-a-2'
+ end
+ end
+
it 'sorts by last contact date', :js do
create(:ci_runner, description: 'runner-1', created_at: '2018-07-12 15:37', contacted_at: '2018-07-12 15:37')
create(:ci_runner, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37')
diff --git a/spec/features/admin/admin_sees_project_statistics_spec.rb b/spec/features/admin/admin_sees_project_statistics_spec.rb
new file mode 100644
index 00000000000..b5323a1c76d
--- /dev/null
+++ b/spec/features/admin/admin_sees_project_statistics_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe "Admin > Admin sees project statistics" do
+ let(:current_user) { create(:admin) }
+
+ before do
+ sign_in(current_user)
+
+ visit admin_project_path(project)
+ end
+
+ context 'when project has statistics' do
+ let(:project) { create(:project, :repository) }
+
+ it "shows project statistics" do
+ expect(page).to have_content("Storage: 0 Bytes (0 Bytes repositories, 0 Bytes wikis, 0 Bytes build artifacts, 0 Bytes LFS)")
+ end
+ end
+
+ context 'when project has no statistics' do
+ let(:project) { create(:project, :repository) { |project| project.statistics.destroy } }
+
+ it "shows 'Storage: Unknown'" do
+ expect(page).to have_content("Storage: Unknown")
+ end
+ end
+end
diff --git a/spec/features/admin/admin_sees_projects_statistics_spec.rb b/spec/features/admin/admin_sees_projects_statistics_spec.rb
new file mode 100644
index 00000000000..6a6f369ac7c
--- /dev/null
+++ b/spec/features/admin/admin_sees_projects_statistics_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe "Admin > Admin sees projects statistics" do
+ let(:current_user) { create(:admin) }
+
+ before do
+ create(:project, :repository)
+ create(:project, :repository) { |project| project.statistics.destroy }
+
+ sign_in(current_user)
+
+ visit admin_projects_path
+ end
+
+ it "shows project statistics for projects that have them" do
+ expect(page.all('.stats').map(&:text)).to contain_exactly("0 Bytes", "Unknown")
+ end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 04f39b807d7..c4dbe23f6b4 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -230,6 +230,13 @@ describe 'Admin updates settings' do
expect(find_field('Username').value).to eq 'test_user'
expect(find('#service_push_channel').value).to eq '#test_channel'
end
+
+ it 'defaults Deployment events to false for chat notification template settings' do
+ first(:link, 'Service Templates').click
+ click_link 'Slack notifications'
+
+ expect(find_field('Deployment')).not_to be_checked
+ end
end
context 'CI/CD page' do
@@ -325,16 +332,19 @@ describe 'Admin updates settings' do
end
context 'Network page' do
- it 'Enable outbound requests' do
+ it 'Changes Outbound requests settings' do
visit network_admin_application_settings_path
page.within('.as-outbound') do
check 'Allow requests to the local network from hooks and services'
+ # Enabled by default
+ uncheck 'Enforce DNS rebinding attack protection'
click_button 'Save changes'
end
expect(page).to have_content "Application settings saved successfully"
expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true
+ expect(Gitlab::CurrentSettings.dns_rebinding_protection_enabled).to be false
end
end
@@ -368,15 +378,50 @@ describe 'Admin updates settings' do
expect(Gitlab::CurrentSettings.pages_domain_verification_enabled?).to be_truthy
expect(page).to have_content "Application settings saved successfully"
end
+
+ context 'When pages_auto_ssl is enabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: true)
+ visit preferences_admin_application_settings_path
+ end
+
+ it "Change Pages Let's Encrypt settings" do
+ page.within('.as-pages') do
+ fill_in 'Email', with: 'my@test.example.com'
+ check "I have read and agree to the Let's Encrypt Terms of Service"
+ click_button 'Save changes'
+ end
+
+ expect(Gitlab::CurrentSettings.lets_encrypt_notification_email).to eq 'my@test.example.com'
+ expect(Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted).to eq true
+ end
+ end
+
+ context 'When pages_auto_ssl is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ visit preferences_admin_application_settings_path
+ end
+
+ it "Doesn't show Let's Encrypt options" do
+ page.within('.as-pages') do
+ expect(page).not_to have_content('Email')
+ end
+ end
+ end
end
def check_all_events
page.check('Active')
page.check('Push')
- page.check('Tag push')
- page.check('Note')
page.check('Issue')
+ page.check('Confidential issue')
page.check('Merge request')
+ page.check('Note')
+ page.check('Confidential note')
+ page.check('Tag push')
page.check('Pipeline')
+ page.check('Wiki page')
+ page.check('Deployment')
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index b1c6f308bc6..29545779a34 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -34,14 +34,11 @@ describe "Admin::Users" do
expect(page).to have_button('Delete user and contributions')
end
- describe "view extra user information", :js do
- it 'does not have the user popover open' do
+ describe "view extra user information" do
+ it 'shows the user popover on hover', :js, :quarantine do
expect(page).not_to have_selector('#__BV_popover_1__')
- end
- it 'shows the user popover on hover' do
first_user_link = page.first('.js-user-link')
-
first_user_link.hover
expect(page).to have_selector('#__BV_popover_1__')
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index dfa1c92ea49..d523e2992db 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe "Dashboard Issues Feed" do
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index ea69ec0319b..4c6175f5590 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -345,7 +345,7 @@ describe 'Issue Boards', :js do
click_link 'Create project label'
- fill_in('new_label_name', with: 'Testing New Label')
+ fill_in('new_label_name', with: 'Testing New Label - with list')
first('.suggest-colors a').click
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
index ee38e756f9e..b1798c11361 100644
--- a/spec/features/boards/sidebar_spec.rb
+++ b/spec/features/boards/sidebar_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Issue Boards', :js do
include BoardHelpers
+ include FilteredSearchHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -129,9 +130,10 @@ describe 'Issue Boards', :js do
click_link 'Unassigned'
end
+ close_dropdown_menu_if_visible
wait_for_requests
- expect(page).to have_content('No assignee')
+ expect(page).to have_content('None')
end
expect(card_two).not_to have_selector('.avatar')
@@ -141,7 +143,7 @@ describe 'Issue Boards', :js do
click_card(card)
page.within(find('.assignee')) do
- expect(page).to have_content('No assignee')
+ expect(page).to have_content('None')
click_button 'assign yourself'
@@ -220,6 +222,21 @@ describe 'Issue Boards', :js do
end
end
+ context 'time tracking' do
+ before do
+ issue2.timelogs.create(time_spent: 14400, user: user)
+ issue2.update!(time_estimate: 28800)
+ end
+
+ it 'shows time tracking progress bar' do
+ click_card(card)
+
+ page.within('.time-tracking') do
+ expect(find('.time-tracking-content .compare-meter')['data-original-title']).to eq('Time remaining: 4h')
+ end
+ end
+ end
+
context 'due date' do
it 'updates due date' do
click_card(card)
@@ -335,14 +352,36 @@ describe 'Issue Boards', :js do
page.within('.labels') do
click_link 'Edit'
+ wait_for_requests
+
+ click_link 'Create project label'
+ fill_in 'new_label_name', with: 'test label'
+ first('.suggest-colors-dropdown a').click
+ click_button 'Create'
+ wait_for_requests
+
+ expect(page).to have_link 'test label'
+ end
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'creates project label and list' do
+ click_card(card)
+
+ page.within('.labels') do
+ click_link 'Edit'
+ wait_for_requests
+
click_link 'Create project label'
fill_in 'new_label_name', with: 'test label'
first('.suggest-colors-dropdown a').click
+ first('.js-add-list').click
click_button 'Create'
wait_for_requests
expect(page).to have_link 'test label'
end
+ expect(page).to have_selector('.board', count: 4)
end
end
diff --git a/spec/features/clusters/cluster_detail_page_spec.rb b/spec/features/clusters/cluster_detail_page_spec.rb
index 0a9c4bcaf12..683c57a97f8 100644
--- a/spec/features/clusters/cluster_detail_page_spec.rb
+++ b/spec/features/clusters/cluster_detail_page_spec.rb
@@ -3,7 +3,11 @@
require 'spec_helper'
describe 'Clusterable > Show page' do
+ include KubernetesHelpers
+
let(:current_user) { create(:user) }
+ let(:cluster_ingress_help_text_selector) { '.js-ingress-domain-help-text' }
+ let(:hide_modifier_selector) { '.hide' }
before do
sign_in(current_user)
@@ -35,7 +39,7 @@ describe 'Clusterable > Show page' do
it 'shows help text with the domain as an alternative to custom domain' do
within '#cluster-integration' do
- expect(page).to have_content('Alternatively 192.168.1.100.nip.io can be used instead of a custom domain')
+ expect(find(cluster_ingress_help_text_selector)).not_to match_css(hide_modifier_selector)
end
end
end
@@ -45,18 +49,87 @@ describe 'Clusterable > Show page' do
visit cluster_path
within '#cluster-integration' do
- expect(page).not_to have_content('can be used instead of a custom domain.')
+ expect(find(cluster_ingress_help_text_selector)).to match_css(hide_modifier_selector)
end
end
end
end
+ shared_examples 'editing a GCP cluster' do
+ before do
+ clusterable.add_maintainer(current_user)
+ visit cluster_path
+ end
+
+ it 'is not able to edit the name, API url, CA certificate nor token' do
+ within('#js-cluster-details') do
+ cluster_name_field = find('.cluster-name')
+ api_url_field = find('#cluster_platform_kubernetes_attributes_api_url')
+ ca_certificate_field = find('#cluster_platform_kubernetes_attributes_ca_cert')
+ token_field = find('#cluster_platform_kubernetes_attributes_token')
+
+ expect(cluster_name_field).to be_readonly
+ expect(api_url_field).to be_readonly
+ expect(ca_certificate_field).to be_readonly
+ expect(token_field).to be_readonly
+ end
+ end
+
+ it 'displays GKE information' do
+ within('#advanced-settings-section') do
+ expect(page).to have_content('Google Kubernetes Engine')
+ expect(page).to have_content('Manage your Kubernetes cluster by visiting')
+ end
+ end
+ end
+
+ shared_examples 'editing a user-provided cluster' do
+ before do
+ stub_kubeclient_discover(cluster.platform.api_url)
+ clusterable.add_maintainer(current_user)
+ visit cluster_path
+ end
+
+ it 'is able to edit the name, API url, CA certificate and token' do
+ within('#js-cluster-details') do
+ cluster_name_field = find('#cluster_name')
+ api_url_field = find('#cluster_platform_kubernetes_attributes_api_url')
+ ca_certificate_field = find('#cluster_platform_kubernetes_attributes_ca_cert')
+ token_field = find('#cluster_platform_kubernetes_attributes_token')
+
+ expect(cluster_name_field).not_to be_readonly
+ expect(api_url_field).not_to be_readonly
+ expect(ca_certificate_field).not_to be_readonly
+ expect(token_field).not_to be_readonly
+ end
+ end
+
+ it 'does not display GKE information' do
+ within('#advanced-settings-section') do
+ expect(page).not_to have_content('Google Kubernetes Engine')
+ expect(page).not_to have_content('Manage your Kubernetes cluster by visiting')
+ end
+ end
+ end
+
context 'when clusterable is a project' do
it_behaves_like 'editing domain' do
let(:clusterable) { create(:project) }
let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
let(:cluster_path) { project_cluster_path(clusterable, cluster) }
end
+
+ it_behaves_like 'editing a GCP cluster' do
+ let(:clusterable) { create(:project) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :project, projects: [clusterable]) }
+ let(:cluster_path) { project_cluster_path(clusterable, cluster) }
+ end
+
+ it_behaves_like 'editing a user-provided cluster' do
+ let(:clusterable) { create(:project) }
+ let(:cluster) { create(:cluster, :provided_by_user, :project, projects: [clusterable]) }
+ let(:cluster_path) { project_cluster_path(clusterable, cluster) }
+ end
end
context 'when clusterable is a group' do
@@ -65,5 +138,17 @@ describe 'Clusterable > Show page' do
let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) }
let(:cluster_path) { group_cluster_path(clusterable, cluster) }
end
+
+ it_behaves_like 'editing a GCP cluster' do
+ let(:clusterable) { create(:group) }
+ let(:cluster) { create(:cluster, :provided_by_gcp, :group, groups: [clusterable]) }
+ let(:cluster_path) { group_cluster_path(clusterable, cluster) }
+ end
+
+ it_behaves_like 'editing a user-provided cluster' do
+ let(:clusterable) { create(:group) }
+ let(:cluster) { create(:cluster, :provided_by_user, :group, groups: [clusterable]) }
+ let(:cluster_path) { group_cluster_path(clusterable, cluster) }
+ end
end
end
diff --git a/spec/features/commits/user_uses_quick_actions_spec.rb b/spec/features/commits/user_uses_quick_actions_spec.rb
index 9a4b7bd2444..4b7e7465df1 100644
--- a/spec/features/commits/user_uses_quick_actions_spec.rb
+++ b/spec/features/commits/user_uses_quick_actions_spec.rb
@@ -22,27 +22,6 @@ describe 'Commit > User uses quick actions', :js do
let(:tag_message) { 'Stable release' }
let(:truncated_commit_sha) { Commit.truncate_sha(commit.sha) }
- it 'tags this commit' do
- add_note("/tag #{tag_name} #{tag_message}")
-
- expect(page).to have_content 'Commands applied'
- expect(page).to have_content "tagged commit #{truncated_commit_sha}"
- expect(page).to have_content tag_name
-
- visit project_tag_path(project, tag_name)
- expect(page).to have_content tag_name
- expect(page).to have_content tag_message
- expect(page).to have_content truncated_commit_sha
- end
-
- describe 'preview', :js do
- it 'removes quick action from note and explains it' do
- preview_note("/tag #{tag_name} #{tag_message}")
-
- expect(page).not_to have_content '/tag'
- expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"}
- expect(page).to have_content tag_name
- end
- end
+ it_behaves_like 'tag quick action'
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 5c6c1c4fd15..2adeb37c98a 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -89,7 +89,7 @@ describe 'Commits' do
context 'Download artifacts' do
before do
- build.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
end
it do
@@ -119,7 +119,7 @@ describe 'Commits' do
context "when logged as reporter" do
before do
project.add_reporter(user)
- build.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
visit pipeline_path(pipeline)
end
@@ -141,7 +141,7 @@ describe 'Commits' do
project.update(
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
public_builds: false)
- build.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: build)
visit pipeline_path(pipeline)
end
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index f4b2b9033ab..48edc764a8e 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -90,25 +90,6 @@ describe 'Cycle Analytics', :js do
end
end
end
-
- context "when my preferred language is Spanish" do
- before do
- user.update_attribute(:preferred_language, 'es')
-
- project.add_maintainer(user)
- sign_in(user)
- visit project_cycle_analytics_path(project)
- wait_for_requests
- end
-
- it 'shows the content in Spanish' do
- expect(page).to have_content('Estado del Pipeline')
- end
-
- it 'resets the language to English' do
- expect(I18n.locale).to eq(:en)
- end
- end
end
context "as a guest" do
diff --git a/spec/features/dashboard/activity_spec.rb b/spec/features/dashboard/activity_spec.rb
index bf91dc121d8..c55dc4523f7 100644
--- a/spec/features/dashboard/activity_spec.rb
+++ b/spec/features/dashboard/activity_spec.rb
@@ -39,6 +39,8 @@ describe 'Dashboard > Activity' do
event
end
+ let(:issue) { create(:issue, project: project) }
+
let!(:merged_event) do
create(:event, :merged, project: project, target: merge_request, author: user)
end
@@ -59,6 +61,10 @@ describe 'Dashboard > Activity' do
create(:event, :closed, project: project, target: milestone, author: user)
end
+ let!(:issue_event) do
+ create(:event, :created, project: project, target: issue, author: user)
+ end
+
before do
project.add_maintainer(user)
@@ -74,6 +80,7 @@ describe 'Dashboard > Activity' do
expect(page).to have_content('closed')
expect(page).to have_content('commented on')
expect(page).to have_content('closed milestone')
+ expect(page).to have_content('opened issue')
end
end
@@ -87,6 +94,7 @@ describe 'Dashboard > Activity' do
expect(page).not_to have_content('accepted')
expect(page).not_to have_content('closed')
expect(page).not_to have_content('commented on')
+ expect(page).not_to have_content('opened issue')
end
end
@@ -100,6 +108,7 @@ describe 'Dashboard > Activity' do
expect(page).to have_content('accepted')
expect(page).not_to have_content('closed')
expect(page).not_to have_content('commented on')
+ expect(page).not_to have_content('opened issue')
end
end
@@ -111,9 +120,10 @@ describe 'Dashboard > Activity' do
expect(page).not_to have_content('pushed new branch')
expect(page).not_to have_content('joined')
expect(page).not_to have_content('accepted')
- expect(page).to have_content('closed')
+ expect(page).not_to have_content('closed')
expect(page).not_to have_content('commented on')
- expect(page).to have_content('closed milestone')
+ expect(page).not_to have_content('closed milestone')
+ expect(page).to have_content('opened issue')
end
end
@@ -127,6 +137,7 @@ describe 'Dashboard > Activity' do
expect(page).not_to have_content('accepted')
expect(page).not_to have_content('closed')
expect(page).to have_content('commented on')
+ expect(page).not_to have_content('opened issue')
end
end
@@ -140,6 +151,7 @@ describe 'Dashboard > Activity' do
expect(page).not_to have_content('accepted')
expect(page).not_to have_content('closed')
expect(page).not_to have_content('commented on')
+ expect(page).not_to have_content('opened issue')
end
end
@@ -155,6 +167,7 @@ describe 'Dashboard > Activity' do
expect(page).not_to have_content('accepted')
expect(page).not_to have_content('closed')
expect(page).not_to have_content('commented on')
+ expect(page).not_to have_content('opened issue')
end
end
end
diff --git a/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
new file mode 100644
index 00000000000..4098dd02141
--- /dev/null
+++ b/spec/features/dashboard/group_dashboard_with_external_authorization_service_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe 'The group dashboard' do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:user) { create(:user) }
+
+ before do
+ sign_in user
+ end
+
+ describe 'The top navigation' do
+ it 'has all the expected links' do
+ visit dashboard_groups_path
+
+ within('.navbar') do
+ expect(page).to have_button('Projects')
+ expect(page).to have_button('Groups')
+ expect(page).to have_link('Activity')
+ expect(page).to have_link('Milestones')
+ expect(page).to have_link('Snippets')
+ end
+ end
+
+ it 'hides some links when an external authorization service is enabled' do
+ enable_external_authorization_service_check
+ visit dashboard_groups_path
+
+ within('.navbar') do
+ expect(page).to have_button('Projects')
+ expect(page).to have_button('Groups')
+ expect(page).not_to have_link('Activity')
+ expect(page).not_to have_link('Milestones')
+ expect(page).to have_link('Snippets')
+ end
+ end
+ end
+end
diff --git a/spec/features/dashboard/issuables_counter_spec.rb b/spec/features/dashboard/issuables_counter_spec.rb
index fbc2e5cc3d3..50b71368e13 100644
--- a/spec/features/dashboard/issuables_counter_spec.rb
+++ b/spec/features/dashboard/issuables_counter_spec.rb
@@ -8,7 +8,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
before do
issue.assignees = [user]
- merge_request.update(assignee: user)
+ merge_request.update(assignees: [user])
sign_in(user)
end
@@ -33,7 +33,7 @@ describe 'Navigation bar counter', :use_clean_rails_memory_store_caching do
expect_counters('merge_requests', '1')
- merge_request.update(assignee: nil)
+ merge_request.update(assignees: [])
user.invalidate_cache_counts
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 9ffa75aee47..0c6713f623c 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -44,16 +44,18 @@ describe 'Dashboard Merge Requests' do
end
context 'merge requests exist' do
+ let(:label) { create(:label) }
+
let!(:assigned_merge_request) do
create(:merge_request,
- assignee: current_user,
+ assignees: [current_user],
source_project: project,
author: create(:user))
end
let!(:assigned_merge_request_from_fork) do
create(:merge_request,
- source_branch: 'markdown', assignee: current_user,
+ source_branch: 'markdown', assignees: [current_user],
target_project: public_project, source_project: forked_project,
author: create(:user))
end
@@ -72,6 +74,14 @@ describe 'Dashboard Merge Requests' do
target_project: public_project, source_project: forked_project)
end
+ let!(:labeled_merge_request) do
+ create(:labeled_merge_request,
+ source_branch: 'labeled',
+ labels: [label],
+ author: current_user,
+ source_project: project)
+ end
+
let!(:other_merge_request) do
create(:merge_request,
source_branch: 'fix',
@@ -90,6 +100,7 @@ describe 'Dashboard Merge Requests' do
expect(page).not_to have_content(authored_merge_request.title)
expect(page).not_to have_content(authored_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title)
+ expect(page).not_to have_content(labeled_merge_request.title)
end
it 'shows authored merge requests', :js do
@@ -98,7 +109,21 @@ describe 'Dashboard Merge Requests' do
expect(page).to have_content(authored_merge_request.title)
expect(page).to have_content(authored_merge_request_from_fork.title)
+ expect(page).to have_content(labeled_merge_request.title)
+
+ expect(page).not_to have_content(assigned_merge_request.title)
+ expect(page).not_to have_content(assigned_merge_request_from_fork.title)
+ expect(page).not_to have_content(other_merge_request.title)
+ end
+
+ it 'shows labeled merge requests', :js do
+ reset_filters
+ input_filtered_search("label:#{label.name}")
+ expect(page).to have_content(labeled_merge_request.title)
+
+ expect(page).not_to have_content(authored_merge_request.title)
+ expect(page).not_to have_content(authored_merge_request_from_fork.title)
expect(page).not_to have_content(assigned_merge_request.title)
expect(page).not_to have_content(assigned_merge_request_from_fork.title)
expect(page).not_to have_content(other_merge_request.title)
diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb
index 9d1c1e3acc7..d1ed64cce7f 100644
--- a/spec/features/dashboard/projects_spec.rb
+++ b/spec/features/dashboard/projects_spec.rb
@@ -112,6 +112,14 @@ describe 'Dashboard Projects' do
expect(first('.project-row')).to have_content(project_with_most_stars.title)
end
+
+ it 'shows tabs to filter by all projects or personal' do
+ visit dashboard_projects_path
+ segmented_button = page.find('.filtered-search-nav .button-filter-group')
+
+ expect(segmented_button).to have_content 'All'
+ expect(segmented_button).to have_content 'Personal'
+ end
end
context 'when on Starred projects tab', :js do
@@ -134,6 +142,12 @@ describe 'Dashboard Projects' do
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)
end
+
+ it 'does not show tabs to filter by all projects or personal' do
+ visit(starred_dashboard_projects_path)
+
+ expect(page).not_to have_content '.filtered-search-nav'
+ end
end
describe 'with a pipeline', :clean_gitlab_redis_shared_state do
diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb
index fd8677feab5..d58e3b2841e 100644
--- a/spec/features/dashboard/todos/todos_spec.rb
+++ b/spec/features/dashboard/todos/todos_spec.rb
@@ -17,6 +17,26 @@ describe 'Dashboard Todos' do
end
end
+ context 'when the todo references a merge request' do
+ let(:referenced_mr) { create(:merge_request, source_project: project) }
+ let(:note) { create(:note, project: project, note: "Check out #{referenced_mr.to_reference}") }
+ let!(:todo) { create(:todo, :mentioned, user: user, project: project, author: author, note: note) }
+
+ before do
+ sign_in(user)
+ visit dashboard_todos_path
+ end
+
+ it 'renders the mr link with the extra attributes' do
+ link = page.find_link(referenced_mr.to_reference)
+
+ expect(link).not_to be_nil
+ expect(link['data-iid']).to eq(referenced_mr.iid.to_s)
+ expect(link['data-project-path']).to eq(referenced_mr.project.full_path)
+ expect(link['data-mr-title']).to eq(referenced_mr.title)
+ end
+ end
+
context 'User has a todo', :js do
before do
create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index cc86114e436..4410c8f887f 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe 'Dashboard > User filters projects' do
let(:user) { create(:user) }
- let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace) }
+ let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace, created_at: 2.seconds.ago, updated_at: 2.seconds.ago) }
let(:user2) { create(:user) }
- let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) }
+ let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace, created_at: 1.second.ago, updated_at: 1.second.ago) }
before do
project.add_maintainer(user)
@@ -14,6 +14,7 @@ describe 'Dashboard > User filters projects' do
describe 'filtering personal projects' do
before do
+ stub_feature_flags(project_list_filter_bar: false)
project2.add_developer(user)
visit dashboard_projects_path
@@ -30,6 +31,7 @@ describe 'Dashboard > User filters projects' do
describe 'filtering starred projects', :js do
before do
+ stub_feature_flags(project_list_filter_bar: false)
user.toggle_star(project)
visit dashboard_projects_path
@@ -42,4 +44,187 @@ describe 'Dashboard > User filters projects' do
expect(page).not_to have_content('You don\'t have starred projects yet')
end
end
+
+ describe 'without search bar', :js do
+ before do
+ stub_feature_flags(project_list_filter_bar: false)
+
+ project2.add_developer(user)
+ visit dashboard_projects_path
+ end
+
+ it 'autocompletes searches upon typing', :js do
+ expect(page).to have_content 'Victorialand'
+ expect(page).to have_content 'Treasure'
+
+ fill_in 'project-filter-form-field', with: 'Lord beerus\n'
+
+ expect(page).not_to have_content 'Victorialand'
+ expect(page).not_to have_content 'Treasure'
+ end
+ end
+
+ describe 'with search bar', :js do
+ before do
+ stub_feature_flags(project_list_filter_bar: true)
+
+ project2.add_developer(user)
+ visit dashboard_projects_path
+ end
+
+ # TODO: move these helpers somewhere more useful
+ def click_sort_direction
+ page.find('.filtered-search-block #filtered-search-sorting-dropdown .reverse-sort-btn').click
+ end
+
+ def select_dropdown_option(selector, label)
+ dropdown = page.find(selector)
+ dropdown.click
+
+ dropdown.find('.dropdown-menu a', text: label, match: :first).click
+ end
+
+ def expect_to_see_projects(sorted_projects)
+ list = page.all('.projects-list .project-name').map(&:text)
+ expect(list).to match(sorted_projects)
+ end
+
+ describe 'Search' do
+ it 'executes when the search button is clicked' do
+ expect(page).to have_content 'Victorialand'
+ expect(page).to have_content 'Treasure'
+
+ fill_in 'project-filter-form-field', with: 'Lord vegeta\n'
+ find('.filtered-search .btn').click
+
+ expect(page).not_to have_content 'Victorialand'
+ expect(page).not_to have_content 'Treasure'
+ end
+
+ it 'will execute when i press enter' do
+ expect(page).to have_content 'Victorialand'
+ expect(page).to have_content 'Treasure'
+
+ fill_in 'project-filter-form-field', with: 'Lord frieza\n'
+ find('#project-filter-form-field').native.send_keys :enter
+
+ expect(page).not_to have_content 'Victorialand'
+ expect(page).not_to have_content 'Treasure'
+ end
+ end
+
+ describe 'Filter' do
+ before do
+ private_project = create(:project, :private, name: 'Private project', namespace: user.namespace)
+ internal_project = create(:project, :internal, name: 'Internal project', namespace: user.namespace)
+
+ private_project.add_maintainer(user)
+ internal_project.add_maintainer(user)
+ end
+
+ it 'filters private projects only' do
+ select_dropdown_option '#filtered-search-visibility-dropdown', 'Private'
+
+ expect(current_url).to match(/visibility_level=0/)
+
+ list = page.all('.projects-list .project-name').map(&:text)
+
+ expect(list).to contain_exactly("Private project", "Treasure", "Victorialand")
+ end
+
+ it 'filters internal projects only' do
+ select_dropdown_option '#filtered-search-visibility-dropdown', 'Internal'
+
+ expect(current_url).to match(/visibility_level=10/)
+
+ list = page.all('.projects-list .project-name').map(&:text)
+
+ expect(list).to contain_exactly('Internal project')
+ end
+
+ it 'filters any project' do
+ select_dropdown_option '#filtered-search-visibility-dropdown', 'Any'
+ list = page.all('.projects-list .project-name').map(&:text)
+
+ expect(list).to contain_exactly("Internal project", "Private project", "Treasure", "Victorialand")
+ end
+ end
+
+ describe 'Sorting' do
+ let(:desc_sorted_project_names) { %w[Treasure Victorialand] }
+
+ before do
+ user.toggle_star(project)
+ user.toggle_star(project2)
+ user2.toggle_star(project2)
+ end
+
+ it 'has all sorting options', :js do
+ sorting_dropdown = page.find('.filtered-search-block #filtered-search-sorting-dropdown')
+
+ expect(sorting_dropdown).to have_css '.reverse-sort-btn'
+
+ sorting_dropdown.click
+
+ ['Last updated', 'Created date', 'Name', 'Stars'].each do |label|
+ expect(sorting_dropdown).to have_content(label)
+ end
+ end
+
+ it 'defaults to "Last updated"', :js do
+ page.find('.filtered-search-block #filtered-search-sorting-dropdown').click
+ active_sorting_option = page.first('.filtered-search-block #filtered-search-sorting-dropdown .is-active')
+
+ expect(active_sorting_option).to have_content 'Last updated'
+ end
+
+ context 'Sorting by name' do
+ it 'sorts the project list' do
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Name'
+
+ expect_to_see_projects(desc_sorted_project_names)
+
+ click_sort_direction
+
+ expect_to_see_projects(desc_sorted_project_names.reverse)
+ end
+ end
+
+ context 'Sorting by Last updated' do
+ it 'sorts the project list' do
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Last updated'
+
+ expect_to_see_projects(desc_sorted_project_names)
+
+ click_sort_direction
+
+ expect_to_see_projects(desc_sorted_project_names.reverse)
+ end
+ end
+
+ context 'Sorting by Created date' do
+ it 'sorts the project list' do
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Created date'
+
+ expect_to_see_projects(desc_sorted_project_names)
+
+ click_sort_direction
+
+ expect_to_see_projects(desc_sorted_project_names.reverse)
+ end
+ end
+
+ context 'Sorting by Stars' do
+ it 'sorts the project list' do
+ select_dropdown_option '#filtered-search-sorting-dropdown', 'Stars'
+
+ expect_to_see_projects(desc_sorted_project_names)
+
+ click_sort_direction
+
+ expect_to_see_projects(desc_sorted_project_names.reverse)
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 8d801161148..b2b3382666a 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -34,7 +34,7 @@ describe 'Expand and collapse diffs', :js do
define_method(file.split('.').first) { file_container(file) }
end
- it 'should show the diff content with a highlighted line when linking to line' do
+ it 'shows the diff content with a highlighted line when linking to line' do
expect(large_diff).not_to have_selector('.code')
expect(large_diff).to have_selector('.nothing-here-block')
@@ -48,7 +48,7 @@ describe 'Expand and collapse diffs', :js do
expect(large_diff).to have_selector('.hll')
end
- it 'should show the diff content when linking to file' do
+ it 'shows the diff content when linking to file' do
expect(large_diff).not_to have_selector('.code')
expect(large_diff).to have_selector('.nothing-here-block')
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
index 8ed4051856e..56f6b1f7eaf 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -68,17 +68,17 @@ describe 'Explore Groups page', :js do
end
describe 'landing component' do
- it 'should show a landing component' do
+ it 'shows a landing component' do
expect(page).to have_content('Below you will find all the groups that are public.')
end
- it 'should be dismissable' do
+ it 'is dismissable' do
find('.dismiss-button').click
expect(page).not_to have_content('Below you will find all the groups that are public.')
end
- it 'should persistently not show once dismissed' do
+ it 'does not show persistently once dismissed' do
find('.dismiss-button').click
visit explore_groups_path
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index d7692181453..f2ab5373d3d 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -25,4 +25,18 @@ describe 'Global search' do
expect(page).to have_selector('.gl-pagination .next')
end
end
+
+ it 'closes the dropdown on blur', :js do
+ visit dashboard_projects_path
+
+ fill_in 'search', with: "a"
+ dropdown = find('.js-dashboard-search-options')
+
+ expect(dropdown[:class]).to include 'show'
+
+ find('#search').send_keys(:backspace)
+ find('body').click
+
+ expect(dropdown[:class]).not_to include 'show'
+ end
end
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index 57e3ddfb39c..fc5777e8c7c 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe 'Group variables', :js do
let(:user) { create(:user) }
let(:group) { create(:group) }
- let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test value', group: group) }
+ let!(:variable) { create(:ci_group_variable, key: 'test_key', value: 'test_value', masked: true, group: group) }
let(:page_path) { group_settings_ci_cd_path(group) }
before do
diff --git a/spec/features/groups/clusters/user_spec.rb b/spec/features/groups/clusters/user_spec.rb
index 2410cd92e3f..84a8691a7f2 100644
--- a/spec/features/groups/clusters/user_spec.rb
+++ b/spec/features/groups/clusters/user_spec.rb
@@ -14,6 +14,7 @@ describe 'User Cluster', :js do
allow(Groups::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+ allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected)
end
context 'when user does not have a cluster and visits cluster index page' do
@@ -69,7 +70,7 @@ describe 'User Cluster', :js do
end
it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
+ expect(page).to have_css('.gl-field-error')
end
end
end
diff --git a/spec/features/groups/group_page_with_external_authorization_service_spec.rb b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
new file mode 100644
index 00000000000..c05c3f4f3d6
--- /dev/null
+++ b/spec/features/groups/group_page_with_external_authorization_service_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'The group page' do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+
+ before do
+ sign_in user
+ group.add_owner(user)
+ end
+
+ def expect_all_sidebar_links
+ within('.nav-sidebar') do
+ expect(page).to have_link('Overview')
+ expect(page).to have_link('Details')
+ expect(page).to have_link('Activity')
+ expect(page).to have_link('Issues')
+ expect(page).to have_link('Merge Requests')
+ expect(page).to have_link('Members')
+ end
+ end
+
+ describe 'The sidebar' do
+ it 'has all the expected links' do
+ visit group_path(group)
+
+ expect_all_sidebar_links
+ end
+
+ it 'shows all project features when policy control is enabled' do
+ stub_application_setting(external_authorization_service_enabled: true)
+
+ visit group_path(group)
+
+ expect_all_sidebar_links
+ end
+
+ it 'hides some links when an external authorization service configured with an url' do
+ enable_external_authorization_service_check
+ visit group_path(group)
+
+ within('.nav-sidebar') do
+ expect(page).to have_link('Overview')
+ expect(page).to have_link('Details')
+ expect(page).not_to have_link('Activity')
+ expect(page).not_to have_link('Contribution Analytics')
+
+ expect(page).not_to have_link('Issues')
+ expect(page).not_to have_link('Merge Requests')
+ expect(page).to have_link('Members')
+ end
+ end
+ end
+end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 378e4d5febc..5cef5f0521f 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -77,6 +77,14 @@ describe 'Edit group settings' do
end
end
+ describe 'project creation level menu' do
+ it 'shows the selection menu' do
+ visit edit_group_path(group)
+
+ expect(page).to have_content('Allowed to create projects')
+ end
+ end
+
describe 'edit group avatar' do
before do
visit edit_group_path(group)
diff --git a/spec/features/groups/members/leave_group_spec.rb b/spec/features/groups/members/leave_group_spec.rb
index 7a91c64d7db..439803f9255 100644
--- a/spec/features/groups/members/leave_group_spec.rb
+++ b/spec/features/groups/members/leave_group_spec.rb
@@ -21,6 +21,20 @@ describe 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
+ it 'guest leaves the group by url param', :js do
+ group.add_guest(user)
+ group.add_owner(other_user)
+
+ visit group_path(group, leave: 1)
+
+ page.accept_confirm
+
+ expect(find('.flash-notice')).to have_content "You left the \"#{group.full_name}\" group"
+ expect(page).to have_content left_group_message(group)
+ expect(current_path).to eq(dashboard_groups_path)
+ expect(group.users).not_to include(user)
+ end
+
it 'guest leaves the group as last member' do
group.add_guest(user)
@@ -32,7 +46,7 @@ describe 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
- it 'owner leaves the group if they is not the last owner' do
+ it 'owner leaves the group if they are not the last owner' do
group.add_owner(user)
group.add_owner(other_user)
@@ -44,7 +58,7 @@ describe 'Groups > Members > Leave group' do
expect(group.users).not_to include(user)
end
- it 'owner can not leave the group if they is a last owner' do
+ it 'owner can not leave the group if they are the last owner' do
group.add_owner(user)
visit group_path(group)
@@ -56,6 +70,14 @@ describe 'Groups > Members > Leave group' do
expect(find(:css, '.project-members-page li', text: user.name)).not_to have_selector(:css, 'a.btn-remove')
end
+ it 'owner can not leave the group by url param if they are the last owner', :js do
+ group.add_owner(user)
+
+ visit group_path(group, leave: 1)
+
+ expect(find('.flash-alert')).to have_content 'You do not have permission to leave this group'
+ end
+
def left_group_message(group)
"You left the \"#{group.name}\""
end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 54a8016c157..59230d6891a 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Group merge requests page' do
@@ -38,7 +40,7 @@ describe 'Group merge requests page' do
context 'when merge request assignee to user' do
before do
- issuable.update!(assignee: user)
+ issuable.update!(assignees: [user])
visit path
end
diff --git a/spec/features/groups/settings/ci_cd_spec.rb b/spec/features/groups/settings/ci_cd_spec.rb
index d422fd18346..5b1a9512c55 100644
--- a/spec/features/groups/settings/ci_cd_spec.rb
+++ b/spec/features/groups/settings/ci_cd_spec.rb
@@ -5,8 +5,8 @@ require 'spec_helper'
describe 'Group CI/CD settings' do
include WaitForRequests
- let(:user) {create(:user)}
- let(:group) {create(:group)}
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
before do
group.add_owner(user)
@@ -36,4 +36,45 @@ describe 'Group CI/CD settings' do
end
end
end
+
+ describe 'Auto DevOps form' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ context 'as owner first visiting group settings' do
+ it 'sees instance enabled badge' do
+ visit group_settings_ci_cd_path(group)
+
+ page.within '#auto-devops-settings' do
+ expect(page).to have_content('instance enabled')
+ end
+ end
+ end
+
+ context 'when Auto DevOps group has been enabled' do
+ it 'sees group enabled badge' do
+ group.update!(auto_devops_enabled: true)
+
+ visit group_settings_ci_cd_path(group)
+
+ page.within '#auto-devops-settings' do
+ expect(page).to have_content('group enabled')
+ end
+ end
+ end
+
+ context 'when Auto DevOps group has been disabled' do
+ it 'does not see a badge' do
+ group.update!(auto_devops_enabled: false)
+
+ visit group_settings_ci_cd_path(group)
+
+ page.within '#auto-devops-settings' do
+ expect(page).not_to have_content('instance enabled')
+ expect(page).not_to have_content('group enabled')
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index c2f32c76422..8e7f78cab81 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -237,7 +237,7 @@ describe 'Group' do
let!(:project) { create(:project, namespace: group) }
let!(:path) { group_path(group) }
- it 'it renders projects and groups on the page' do
+ it 'renders projects and groups on the page' do
visit path
wait_for_requests
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index e24b1f4349d..bcd2b90d3bb 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -82,15 +82,15 @@ describe 'Help Pages' do
visit help_path
end
- it 'should display custom help page text' do
+ it 'displays custom help page text' do
expect(page).to have_text "My Custom Text"
end
- it 'should hide marketing content when enabled' do
+ it 'hides marketing content when enabled' do
expect(page).not_to have_link "Get a support subscription"
end
- it 'should use a custom support url' do
+ it 'uses a custom support url' do
expect(page).to have_link "See our website for getting help", href: "http://example.com/help"
end
end
diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb
new file mode 100644
index 00000000000..185349219a7
--- /dev/null
+++ b/spec/features/ide/user_opens_merge_request_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'IDE merge request', :js do
+ let(:merge_request) { create(:merge_request, :with_diffs, :simple, source_project: project) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:user) { project.owner }
+
+ before do
+ sign_in(user)
+
+ visit(merge_request_path(merge_request))
+ end
+
+ it 'user opens merge request' do
+ click_link 'Open in Web IDE'
+
+ wait_for_requests
+
+ expect(page).to have_selector('.monaco-diff-editor')
+ end
+end
diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/conversational_development_index_spec.rb
index d8be554d734..713cd944f8c 100644
--- a/spec/features/instance_statistics/conversational_development_index_spec.rb
+++ b/spec/features/instance_statistics/conversational_development_index_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Conversational Development Index' do
diff --git a/spec/features/issuables/issuable_list_spec.rb b/spec/features/issuables/issuable_list_spec.rb
index 7b6e9cd66b2..225b858742d 100644
--- a/spec/features/issuables/issuable_list_spec.rb
+++ b/spec/features/issuables/issuable_list_spec.rb
@@ -76,7 +76,7 @@ describe 'issuable list' do
create(:issue, project: project, author: user)
else
create(:merge_request, source_project: project, source_branch: generate(:branch))
- source_branch = FFaker::Name.name
+ source_branch = FFaker::Lorem.characters(8)
pipeline = create(:ci_empty_pipeline, project: project, ref: source_branch, status: %w(running failed success).sample, sha: 'any')
create(:merge_request, title: FFaker::Lorem.sentence, source_project: project, source_branch: source_branch, head_pipeline: pipeline)
end
diff --git a/spec/features/issuables/markdown_references/internal_references_spec.rb b/spec/features/issuables/markdown_references/internal_references_spec.rb
index 23385ba65fc..870e92b8de8 100644
--- a/spec/features/issuables/markdown_references/internal_references_spec.rb
+++ b/spec/features/issuables/markdown_references/internal_references_spec.rb
@@ -70,7 +70,7 @@ describe "Internal references", :js do
page.within("#merge-requests ul") do
expect(page).to have_content(private_project_merge_request.title)
- expect(page).to have_css(".merge-request-status")
+ expect(page).to have_css(".ic-issue-open-m")
end
expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
diff --git a/spec/features/issuables/shortcuts_issuable_spec.rb b/spec/features/issuables/shortcuts_issuable_spec.rb
index a0ae6720a9f..a19101366a0 100644
--- a/spec/features/issuables/shortcuts_issuable_spec.rb
+++ b/spec/features/issuables/shortcuts_issuable_spec.rb
@@ -13,7 +13,7 @@ describe 'Blob shortcuts', :js do
end
shared_examples "quotes the selected text" do
- it "quotes the selected text" do
+ it "quotes the selected text", :quarantine do
select_element('.note-text')
find('body').native.send_key('r')
diff --git a/spec/features/issuables/sorting_list_spec.rb b/spec/features/issuables/sorting_list_spec.rb
index 0601dd47c03..3a46a4e0167 100644
--- a/spec/features/issuables/sorting_list_spec.rb
+++ b/spec/features/issuables/sorting_list_spec.rb
@@ -86,26 +86,26 @@ describe 'Sort Issuable List' do
expect(last_merge_request).to include(first_created_issuable.title)
end
end
+ end
- context 'custom sorting' do
- let(:issuable_type) { :merge_request }
+ context 'custom sorting' do
+ let(:issuable_type) { :merge_request }
- it 'supports sorting in asc and desc order' do
- visit_merge_requests_with_state(project, 'open')
+ it 'supports sorting in asc and desc order' do
+ visit_merge_requests_with_state(project, 'open')
- page.within('.issues-other-filters') do
- click_button('Created date')
- click_link('Last updated')
- end
+ page.within('.issues-other-filters') do
+ click_button('Created date')
+ click_link('Last updated')
+ end
- expect(first_merge_request).to include(last_updated_issuable.title)
- expect(last_merge_request).to include(first_updated_issuable.title)
+ expect(first_merge_request).to include(last_updated_issuable.title)
+ expect(last_merge_request).to include(first_updated_issuable.title)
- find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click
+ find('.issues-other-filters .filter-dropdown-container .qa-reverse-sort').click
- expect(first_merge_request).to include(first_updated_issuable.title)
- expect(last_merge_request).to include(last_updated_issuable.title)
- end
+ expect(first_merge_request).to include(first_updated_issuable.title)
+ expect(last_merge_request).to include(last_updated_issuable.title)
end
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index e0b1e286dee..75313442b65 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -42,7 +42,7 @@ describe 'Dropdown assignee', :js do
expect(page).to have_css(js_dropdown_assignee, visible: false)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened' do
slow_requests do
# We aren't using `input_filtered_search` because we want to see the loading indicator
filtered_search.set('assignee:')
@@ -51,13 +51,13 @@ describe 'Dropdown assignee', :js do
end
end
- it 'should hide loading indicator when loaded' do
+ it 'hides loading indicator when loaded' do
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(find(js_dropdown_assignee)).not_to have_css('.filter-dropdown-loading')
end
- it 'should load all the assignees when opened' do
+ it 'loads all the assignees when opened' do
input_filtered_search('assignee:', submit: false, extra_space: false)
expect(dropdown_assignee_size).to eq(4)
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index bedc61b9eed..bc8d9bc8450 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -50,7 +50,7 @@ describe 'Dropdown author', :js do
expect(page).to have_css(js_dropdown_author, visible: false)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened' do
slow_requests do
filtered_search.set('author:')
@@ -58,13 +58,13 @@ describe 'Dropdown author', :js do
end
end
- it 'should hide loading indicator when loaded' do
+ it 'hides loading indicator when loaded' do
send_keys_to_filtered_search('author:')
expect(page).not_to have_css('#js-dropdown-author .filter-dropdown-loading')
end
- it 'should load all the authors when opened' do
+ it 'loads all the authors when opened' do
send_keys_to_filtered_search('author:')
expect(dropdown_author_size).to eq(4)
diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
index f36d4e8f23f..a5c3ab7e7d0 100644
--- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb
@@ -69,7 +69,7 @@ describe 'Dropdown emoji', :js do
expect(page).to have_css(js_dropdown_emoji, visible: false)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened' do
slow_requests do
filtered_search.set('my-reaction:')
@@ -77,13 +77,13 @@ describe 'Dropdown emoji', :js do
end
end
- it 'should hide loading indicator when loaded' do
+ it 'hides loading indicator when loaded' do
send_keys_to_filtered_search('my-reaction:')
expect(page).not_to have_css('#js-dropdown-my-reaction .filter-dropdown-loading')
end
- it 'should load all the emojis when opened' do
+ it 'loads all the emojis when opened' do
send_keys_to_filtered_search('my-reaction:')
expect(dropdown_emoji_size).to eq(4)
diff --git a/spec/features/issues/filtered_search/dropdown_hint_spec.rb b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
index 096756f19cc..1f4e9e79179 100644
--- a/spec/features/issues/filtered_search/dropdown_hint_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_hint_spec.rb
@@ -80,7 +80,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
- expect_tokens([{ name: 'author' }])
+ expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty
end
@@ -89,7 +89,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect_tokens([{ name: 'assignee' }])
+ expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
@@ -98,7 +98,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect_tokens([{ name: 'milestone' }])
+ expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
@@ -107,7 +107,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
- expect_tokens([{ name: 'label' }])
+ expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
@@ -116,7 +116,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-my-reaction', visible: true)
- expect_tokens([{ name: 'my-reaction' }])
+ expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
@@ -125,7 +125,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-confidential', visible: true)
- expect_tokens([{ name: 'confidential' }])
+ expect_tokens([{ name: 'Confidential' }])
expect_filtered_search_input_empty
end
end
@@ -137,7 +137,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-author', visible: true)
- expect_tokens([{ name: 'author' }])
+ expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty
end
@@ -147,7 +147,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-assignee', visible: true)
- expect_tokens([{ name: 'assignee' }])
+ expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
@@ -157,7 +157,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-milestone', visible: true)
- expect_tokens([{ name: 'milestone' }])
+ expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
@@ -167,7 +167,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-label', visible: true)
- expect_tokens([{ name: 'label' }])
+ expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
@@ -177,7 +177,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-my-reaction', visible: true)
- expect_tokens([{ name: 'my-reaction' }])
+ expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
end
@@ -189,7 +189,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('author')
- expect_tokens([{ name: 'author' }])
+ expect_tokens([{ name: 'Author' }])
expect_filtered_search_input_empty
end
@@ -199,7 +199,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('assignee')
- expect_tokens([{ name: 'assignee' }])
+ expect_tokens([{ name: 'Assignee' }])
expect_filtered_search_input_empty
end
@@ -209,7 +209,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('milestone')
- expect_tokens([{ name: 'milestone' }])
+ expect_tokens([{ name: 'Milestone' }])
expect_filtered_search_input_empty
end
@@ -219,7 +219,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('label')
- expect_tokens([{ name: 'label' }])
+ expect_tokens([{ name: 'Label' }])
expect_filtered_search_input_empty
end
@@ -229,7 +229,7 @@ describe 'Dropdown hint', :js do
filtered_search.send_keys(:backspace)
click_hint('my-reaction')
- expect_tokens([{ name: 'my-reaction' }])
+ expect_tokens([{ name: 'My-reaction' }])
expect_filtered_search_input_empty
end
end
@@ -247,7 +247,7 @@ describe 'Dropdown hint', :js do
expect(page).to have_css(js_dropdown_hint, visible: false)
expect(page).to have_css('#js-dropdown-wip', visible: true)
- expect_tokens([{ name: 'wip' }])
+ expect_tokens([{ name: 'WIP' }])
expect_filtered_search_input_empty
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
index b330eafe1d1..7a6f76cb382 100644
--- a/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_milestone_spec.rb
@@ -49,7 +49,7 @@ describe 'Dropdown milestone', :js do
expect(page).to have_css(js_dropdown_milestone, visible: false)
end
- it 'should show loading indicator when opened' do
+ it 'shows loading indicator when opened' do
slow_requests do
filtered_search.set('milestone:')
@@ -57,13 +57,13 @@ describe 'Dropdown milestone', :js do
end
end
- it 'should hide loading indicator when loaded' do
+ it 'hides loading indicator when loaded' do
filtered_search.set('milestone:')
expect(find(js_dropdown_milestone)).not_to have_css('.filter-dropdown-loading')
end
- it 'should load all the milestones when opened' do
+ it 'loads all the milestones when opened' do
filtered_search.set('milestone:')
expect(filter_dropdown).to have_selector('.filter-dropdown .filter-dropdown-item', count: 6)
@@ -139,7 +139,7 @@ describe 'Dropdown milestone', :js do
expect_filtered_search_input_empty
end
- it 'fills in the milestone name when the milestone is partially filled' do
+ it 'fills in the milestone name when the milestone is partially filled', :quarantine do
filtered_search.send_keys('v')
click_milestone(milestone.title)
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index c4468922883..da23aea1fc9 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -86,7 +86,7 @@ describe 'Search bar', :js do
expect(find('#js-dropdown-hint')).to have_selector('.filter-dropdown .filter-dropdown-item', count: original_size)
end
- it 'resets the dropdown filters' do
+ it 'resets the dropdown filters', :quarantine do
filtered_search.click
hint_offset = get_left_style(find('#js-dropdown-hint')['style'])
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index a4c34ce85f0..9fd661d80ae 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -59,13 +59,6 @@ describe 'Visual tokens', :js do
expect(page).to have_css('#js-dropdown-author', visible: false)
end
- it 'ends editing mode when scroll container is clicked' do
- find('.scroll-container').click
-
- expect_filtered_search_input_empty
- expect(page).to have_css('#js-dropdown-author', visible: false)
- end
-
describe 'selecting different author from dropdown' do
before do
filter_author_dropdown.find('.filter-dropdown-item .dropdown-light-content', text: "@#{user_rock.username}").click
@@ -109,13 +102,6 @@ describe 'Visual tokens', :js do
expect(page).to have_css('#js-dropdown-assignee', visible: false)
end
- it 'ends editing mode when scroll container is clicked' do
- find('.scroll-container').click
-
- expect_filtered_search_input_empty
- expect(page).to have_css('#js-dropdown-assignee', visible: false)
- end
-
describe 'selecting static option from dropdown' do
before do
find("#js-dropdown-assignee").find('.filter-dropdown-item', text: 'None').click
@@ -167,13 +153,6 @@ describe 'Visual tokens', :js do
expect_filtered_search_input_empty
expect(page).to have_css('#js-dropdown-milestone', visible: false)
end
-
- it 'ends editing mode when scroll container is clicked' do
- find('.scroll-container').click
-
- expect_filtered_search_input_empty
- expect(page).to have_css('#js-dropdown-milestone', visible: false)
- end
end
describe 'editing label token' do
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index f2e4c5779df..597af566f9c 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -13,6 +13,8 @@ describe 'New/edit issue', :js do
let!(:issue) { create(:issue, project: project, assignees: [user], milestone: milestone) }
before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+
project.add_maintainer(user)
project.add_maintainer(user2)
sign_in(user)
@@ -30,8 +32,8 @@ describe 'New/edit issue', :js do
# the original method, resulting in infinite recursion when called.
# This is likely a bug with helper modules included into dynamically generated view classes.
# To work around this, we have to hold on to and call to the original implementation manually.
- original_issue_dropdown_options = FormHelper.instance_method(:issue_assignees_dropdown_options)
- allow_any_instance_of(FormHelper).to receive(:issue_assignees_dropdown_options).and_wrap_original do |original, *args|
+ original_issue_dropdown_options = FormHelper.instance_method(:assignees_dropdown_options)
+ allow_any_instance_of(FormHelper).to receive(:assignees_dropdown_options).and_wrap_original do |original, *args|
options = original_issue_dropdown_options.bind(original.receiver).call(*args)
options[:data][:per_page] = 2
@@ -45,7 +47,7 @@ describe 'New/edit issue', :js do
wait_for_requests
end
- it 'should display selected users even if they are not part of the original API call' do
+ it 'displays selected users even if they are not part of the original API call' do
find('.dropdown-input-field').native.send_keys user2.name
page.within '.dropdown-menu-user' do
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index 986f3823275..8eb413bdd8d 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -278,12 +278,7 @@ describe 'GFM autocomplete', :js do
end
end
- # This context has just one example in each contexts in order to improve spec performance.
- context 'labels', :quarantine do
- let!(:backend) { create(:label, project: project, title: 'backend') }
- let!(:bug) { create(:label, project: project, title: 'bug') }
- let!(:feature_proposal) { create(:label, project: project, title: 'feature proposal') }
-
+ context 'labels' do
it 'opens autocomplete menu for Labels when field starts with text with item escaping HTML characters' do
create(:label, project: project, title: label_xss_title)
@@ -298,83 +293,6 @@ describe 'GFM autocomplete', :js do
expect(find('.atwho-view-ul').text).to have_content('alert label')
end
end
-
- context 'when no labels are assigned' do
- it 'shows labels' do
- note = find('#note-body')
-
- # It should show all the labels on "~".
- type(note, '~')
- wait_for_requests
- expect_labels(shown: [backend, bug, feature_proposal])
-
- # It should show all the labels on "/label ~".
- type(note, '/label ~')
- expect_labels(shown: [backend, bug, feature_proposal])
-
- # It should show all the labels on "/relabel ~".
- type(note, '/relabel ~')
- expect_labels(shown: [backend, bug, feature_proposal])
-
- # It should show no labels on "/unlabel ~".
- type(note, '/unlabel ~')
- expect_labels(not_shown: [backend, bug, feature_proposal])
- end
- end
-
- context 'when some labels are assigned' do
- before do
- issue.labels << [backend]
- end
-
- it 'shows labels' do
- note = find('#note-body')
-
- # It should show all the labels on "~".
- type(note, '~')
- wait_for_requests
- expect_labels(shown: [backend, bug, feature_proposal])
-
- # It should show only unset labels on "/label ~".
- type(note, '/label ~')
- expect_labels(shown: [bug, feature_proposal], not_shown: [backend])
-
- # It should show all the labels on "/relabel ~".
- type(note, '/relabel ~')
- expect_labels(shown: [backend, bug, feature_proposal])
-
- # It should show only set labels on "/unlabel ~".
- type(note, '/unlabel ~')
- expect_labels(shown: [backend], not_shown: [bug, feature_proposal])
- end
- end
-
- context 'when all labels are assigned' do
- before do
- issue.labels << [backend, bug, feature_proposal]
- end
-
- it 'shows labels' do
- note = find('#note-body')
-
- # It should show all the labels on "~".
- type(note, '~')
- wait_for_requests
- expect_labels(shown: [backend, bug, feature_proposal])
-
- # It should show no labels on "/label ~".
- type(note, '/label ~')
- expect_labels(not_shown: [backend, bug, feature_proposal])
-
- # It should show all the labels on "/relabel ~".
- type(note, '/relabel ~')
- expect_labels(shown: [backend, bug, feature_proposal])
-
- # It should show all the labels on "/unlabel ~".
- type(note, '/unlabel ~')
- expect_labels(shown: [backend, bug, feature_proposal])
- end
- end
end
shared_examples 'autocomplete suggestions' do
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 76bc93e9766..791bd003597 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -26,7 +26,7 @@ describe 'Issue Detail', :js do
wait_for_requests
end
- it 'should encode the description to prevent xss issues' do
+ it 'encodes the description to prevent xss issues' do
page.within('.issuable-details .detail-page-description') do
expect(page).to have_selector('img', count: 1)
expect(find('img')['onerror']).to be_nil
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index 3050f23c130..321da8f44d7 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -130,7 +130,7 @@ describe 'Issue Sidebar' do
end
end
- context 'creating a project label', :js do
+ context 'creating a project label', :js, :quarantine do
before do
page.within('.block.labels') do
click_link 'Create project'
diff --git a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
index 693ad89069c..b69fba0db00 100644
--- a/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
+++ b/spec/features/issues/user_creates_branch_and_merge_request_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
describe 'User creates branch and merge request on issue page', :js do
+ let(:membership_level) { :developer }
let(:user) { create(:user) }
let!(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, title: 'Cherry-Coloured Funk') }
@@ -17,7 +18,7 @@ describe 'User creates branch and merge request on issue page', :js do
context 'when signed in' do
before do
- project.add_developer(user)
+ project.add_user(user, membership_level)
sign_in(user)
end
@@ -28,7 +29,7 @@ describe 'User creates branch and merge request on issue page', :js do
end
# In order to improve tests performance, all UI checks are placed in this test.
- it 'shows elements' do
+ it 'shows elements', :quarantine do
button_create_merge_request = find('.js-create-merge-request')
button_toggle_dropdown = find('.create-mr-dropdown-wrap .dropdown-toggle')
@@ -138,7 +139,7 @@ describe 'User creates branch and merge request on issue page', :js do
visit project_issue_path(project, issue)
end
- it 'disables the create branch button' do
+ it 'disables the create branch button', :quarantine do
expect(page).to have_css('.create-mr-dropdown-wrap .unavailable:not(.hidden)')
expect(page).to have_css('.create-mr-dropdown-wrap .available.hidden', visible: false)
expect(page).to have_content /Related merge requests/
@@ -167,6 +168,39 @@ describe 'User creates branch and merge request on issue page', :js do
expect(page).not_to have_css('.create-mr-dropdown-wrap')
end
end
+
+ context 'when related branch exists' do
+ let!(:project) { create(:project, :repository, :private) }
+ let(:branch_name) { "#{issue.iid}-foo" }
+
+ before do
+ project.repository.create_branch(branch_name, 'master')
+
+ visit project_issue_path(project, issue)
+ end
+
+ context 'when user is developer' do
+ it 'shows related branches' do
+ expect(page).to have_css('#related-branches')
+
+ wait_for_requests
+
+ expect(page).to have_content(branch_name)
+ end
+ end
+
+ context 'when user is guest' do
+ let(:membership_level) { :guest }
+
+ it 'does not show related branches' do
+ expect(page).not_to have_css('#related-branches')
+
+ wait_for_requests
+
+ expect(page).not_to have_content(branch_name)
+ end
+ end
+ end
end
private
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index e60486f6dcb..0f604db870f 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -93,4 +93,22 @@ describe "User creates issue" do
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") }
+
+ before do
+ project.add_developer(user_special)
+ sign_in(user_special)
+
+ visit(new_project_issue_path(project))
+ end
+
+ it "will correctly escape user names with an apostrophe when clicking 'Assign to me'", :js do
+ first('.assign-to-me-link').click
+
+ expect(page).to have_content(user_special.name)
+ expect(page.find('input[name="issue[assignee_ids][]"]', visible: false)['data-meta']).to eq(user_special.name)
+ end
+ end
end
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
index afa425c2cec..d117620a2b1 100644
--- a/spec/features/issues/user_interacts_with_awards_spec.rb
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -14,7 +14,7 @@ describe 'User interacts with awards' do
visit(project_issue_path(project, issue))
end
- it 'toggles the thumbsup award emoji' do
+ it 'toggles the thumbsup award emoji', :quarantine do
page.within('.awards') do
thumbsup = page.first('.award-control')
thumbsup.click
@@ -75,7 +75,7 @@ describe 'User interacts with awards' do
end
end
- it 'shows the list of award emoji categories' do
+ it 'shows the list of award emoji categories', :quarantine do
page.within('.awards') do
page.find('.js-add-award').click
end
diff --git a/spec/features/issues/user_uses_quick_actions_spec.rb b/spec/features/issues/user_uses_quick_actions_spec.rb
index 27cffdc5f8b..86b15b1d980 100644
--- a/spec/features/issues/user_uses_quick_actions_spec.rb
+++ b/spec/features/issues/user_uses_quick_actions_spec.rb
@@ -1,365 +1,68 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe 'Issues > User uses quick actions', :js do
include Spec::Support::Helpers::Features::NotesHelpers
- it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do
+ context "issuable common quick actions" do
+ let(:new_url_opts) { {} }
+ let(:maintainer) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let!(:label_bug) { create(:label, project: project, title: 'bug') }
+ let!(:label_feature) { create(:label, project: project, title: 'feature') }
+ let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
let(:issuable) { create(:issue, project: project) }
+ let(:source_issuable) { create(:issue, project: project, milestone: milestone, labels: [label_bug, label_feature])}
+
+ it_behaves_like 'assign quick action', :issue
+ it_behaves_like 'unassign quick action', :issue
+ it_behaves_like 'close quick action', :issue
+ it_behaves_like 'reopen quick action', :issue
+ it_behaves_like 'title quick action', :issue
+ it_behaves_like 'todo quick action', :issue
+ it_behaves_like 'done quick action', :issue
+ it_behaves_like 'subscribe quick action', :issue
+ it_behaves_like 'unsubscribe quick action', :issue
+ it_behaves_like 'lock quick action', :issue
+ it_behaves_like 'unlock quick action', :issue
+ it_behaves_like 'milestone quick action', :issue
+ it_behaves_like 'remove_milestone quick action', :issue
+ it_behaves_like 'label quick action', :issue
+ it_behaves_like 'unlabel quick action', :issue
+ it_behaves_like 'relabel quick action', :issue
+ it_behaves_like 'award quick action', :issue
+ it_behaves_like 'estimate quick action', :issue
+ it_behaves_like 'remove_estimate quick action', :issue
+ it_behaves_like 'spend quick action', :issue
+ it_behaves_like 'remove_time_spent quick action', :issue
+ it_behaves_like 'shrug quick action', :issue
+ it_behaves_like 'tableflip quick action', :issue
+ it_behaves_like 'copy_metadata quick action', :issue
+ it_behaves_like 'issuable time tracker', :issue
end
describe 'issue-only commands' do
let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
before do
project.add_maintainer(user)
sign_in(user)
visit project_issue_path(project, issue)
+ wait_for_all_requests
end
after do
wait_for_requests
end
- describe 'time tracking' do
- let(:issue) { create(:issue, project: project) }
-
- before do
- visit project_issue_path(project, issue)
- end
-
- it_behaves_like 'issuable time tracker'
- end
-
- describe 'adding a due date from note' do
- let(:issue) { create(:issue, project: project) }
-
- context 'when the current user can update the due date' do
- it 'does not create a note, and sets the due date accordingly' do
- add_note("/due 2016-08-28")
-
- expect(page).not_to have_content '/due 2016-08-28'
- expect(page).to have_content 'Commands applied'
-
- issue.reload
-
- expect(issue.due_date).to eq Date.new(2016, 8, 28)
- end
- end
-
- context 'when the current user cannot update the due date' do
- let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- gitlab_sign_out
- sign_in(guest)
- visit project_issue_path(project, issue)
- end
-
- it 'does not create a note, and sets the due date accordingly' do
- add_note("/due 2016-08-28")
-
- expect(page).not_to have_content 'Commands applied'
-
- issue.reload
-
- expect(issue.due_date).to be_nil
- end
- end
- end
-
- describe 'removing a due date from note' do
- let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
-
- context 'when the current user can update the due date' do
- it 'does not create a note, and removes the due date accordingly' do
- expect(issue.due_date).to eq Date.new(2016, 8, 28)
-
- add_note("/remove_due_date")
-
- expect(page).not_to have_content '/remove_due_date'
- expect(page).to have_content 'Commands applied'
-
- issue.reload
-
- expect(issue.due_date).to be_nil
- end
- end
-
- context 'when the current user cannot update the due date' do
- let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- gitlab_sign_out
- sign_in(guest)
- visit project_issue_path(project, issue)
- end
-
- it 'does not create a note, and sets the due date accordingly' do
- add_note("/remove_due_date")
-
- expect(page).not_to have_content 'Commands applied'
-
- issue.reload
-
- expect(issue.due_date).to eq Date.new(2016, 8, 28)
- end
- end
- end
-
- describe 'toggling the WIP prefix from the title from note' do
- let(:issue) { create(:issue, project: project) }
-
- it 'does not recognize the command nor create a note' do
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- end
- end
-
- describe 'mark issue as duplicate' do
- let(:issue) { create(:issue, project: project) }
- let(:original_issue) { create(:issue, project: project) }
-
- context 'when the current user can update issues' do
- it 'does not create a note, and marks the issue as a duplicate' do
- add_note("/duplicate ##{original_issue.to_reference}")
-
- expect(page).not_to have_content "/duplicate #{original_issue.to_reference}"
- expect(page).to have_content 'Commands applied'
- expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
-
- expect(issue.reload).to be_closed
- end
- end
-
- context 'when the current user cannot update the issue' do
- let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- gitlab_sign_out
- sign_in(guest)
- visit project_issue_path(project, issue)
- end
-
- it 'does not create a note, and does not mark the issue as a duplicate' do
- add_note("/duplicate ##{original_issue.to_reference}")
-
- expect(page).not_to have_content 'Commands applied'
- expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
-
- expect(issue.reload).to be_open
- end
- end
- end
-
- describe 'make issue confidential' do
- let(:issue) { create(:issue, project: project) }
- let(:original_issue) { create(:issue, project: project) }
-
- context 'when the current user can update issues' do
- it 'does not create a note, and marks the issue as confidential' do
- add_note("/confidential")
-
- expect(page).not_to have_content "/confidential"
- expect(page).to have_content 'Commands applied'
- expect(page).to have_content "made the issue confidential"
-
- expect(issue.reload).to be_confidential
- end
- end
-
- context 'when the current user cannot update the issue' do
- let(:guest) { create(:user) }
- before do
- project.add_guest(guest)
- gitlab_sign_out
- sign_in(guest)
- visit project_issue_path(project, issue)
- end
-
- it 'does not create a note, and does not mark the issue as confidential' do
- add_note("/confidential")
-
- expect(page).not_to have_content 'Commands applied'
- expect(page).not_to have_content "made the issue confidential"
-
- expect(issue.reload).not_to be_confidential
- end
- end
- end
-
- describe 'move the issue to another project' do
- let(:issue) { create(:issue, project: project) }
-
- context 'when the project is valid' do
- let(:target_project) { create(:project, :public) }
-
- before do
- target_project.add_maintainer(user)
- gitlab_sign_out
- sign_in(user)
- visit project_issue_path(project, issue)
- end
-
- it 'moves the issue' do
- add_note("/move #{target_project.full_path}")
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_closed
-
- visit project_issue_path(target_project, issue)
-
- expect(page).to have_content 'Issues 1'
- end
- end
-
- context 'when the project is valid but the user not authorized' do
- let(:project_unauthorized) { create(:project, :public) }
-
- before do
- gitlab_sign_out
- sign_in(user)
- visit project_issue_path(project, issue)
- end
-
- it 'does not move the issue' do
- add_note("/move #{project_unauthorized.full_path}")
-
- wait_for_requests
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_open
- end
- end
-
- context 'when the project is invalid' do
- before do
- gitlab_sign_out
- sign_in(user)
- visit project_issue_path(project, issue)
- end
-
- it 'does not move the issue' do
- add_note("/move not/valid")
-
- expect(page).not_to have_content 'Commands applied'
- expect(issue.reload).to be_open
- end
- end
-
- context 'when the user issues multiple commands' do
- let(:target_project) { create(:project, :public) }
- let(:milestone) { create(:milestone, title: '1.0', project: project) }
- let(:target_milestone) { create(:milestone, title: '1.0', project: target_project) }
- let(:bug) { create(:label, project: project, title: 'bug') }
- let(:wontfix) { create(:label, project: project, title: 'wontfix') }
- let(:bug_target) { create(:label, project: target_project, title: 'bug') }
- let(:wontfix_target) { create(:label, project: target_project, title: 'wontfix') }
-
- before do
- target_project.add_maintainer(user)
- gitlab_sign_out
- sign_in(user)
- visit project_issue_path(project, issue)
- end
-
- it 'applies the commands to both issues and moves the issue' do
- add_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_closed
-
- visit project_issue_path(target_project, issue)
-
- expect(page).to have_content 'bug'
- expect(page).to have_content 'wontfix'
- expect(page).to have_content '1.0'
-
- visit project_issue_path(project, issue)
- expect(page).to have_content 'Closed'
- expect(page).to have_content 'bug'
- expect(page).to have_content 'wontfix'
- expect(page).to have_content '1.0'
- end
-
- it 'moves the issue and applies the commands to both issues' do
- add_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
-
- expect(page).to have_content 'Commands applied'
- expect(issue.reload).to be_closed
-
- visit project_issue_path(target_project, issue)
-
- expect(page).to have_content 'bug'
- expect(page).to have_content 'wontfix'
- expect(page).to have_content '1.0'
-
- visit project_issue_path(project, issue)
- expect(page).to have_content 'Closed'
- expect(page).to have_content 'bug'
- expect(page).to have_content 'wontfix'
- expect(page).to have_content '1.0'
- end
- end
- end
-
- describe 'create a merge request starting from an issue' do
- let(:project) { create(:project, :public, :repository) }
- let(:issue) { create(:issue, project: project) }
-
- def expect_mr_quickaction(success)
- expect(page).to have_content 'Commands applied'
-
- if success
- expect(page).to have_content 'created merge request'
- else
- expect(page).not_to have_content 'created merge request'
- end
- end
-
- it "doesn't create a merge request when the branch name is invalid" do
- add_note("/create_merge_request invalid branch name")
-
- wait_for_requests
-
- expect_mr_quickaction(false)
- end
-
- it "doesn't create a merge request when a branch with that name already exists" do
- add_note("/create_merge_request feature")
-
- wait_for_requests
-
- expect_mr_quickaction(false)
- end
-
- it 'creates a new merge request using issue iid and title as branch name when the branch name is empty' do
- add_note("/create_merge_request")
-
- wait_for_requests
-
- expect_mr_quickaction(true)
-
- created_mr = project.merge_requests.last
- expect(created_mr.source_branch).to eq(issue.to_branch_name)
-
- visit project_merge_request_path(project, created_mr)
- expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
- end
-
- it 'creates a merge request using the given branch name' do
- branch_name = '1-feature'
- add_note("/create_merge_request #{branch_name}")
-
- expect_mr_quickaction(true)
-
- created_mr = project.merge_requests.last
- expect(created_mr.source_branch).to eq(branch_name)
-
- visit project_merge_request_path(project, created_mr)
- expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
- end
- end
+ it_behaves_like 'confidential quick action'
+ it_behaves_like 'remove_due_date quick action'
+ it_behaves_like 'duplicate quick action'
+ it_behaves_like 'create_merge_request quick action'
+ it_behaves_like 'due quick action'
+ it_behaves_like 'move quick action'
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 406e80e91aa..5ee9425c491 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Issues' do
@@ -91,7 +93,7 @@ describe 'Issues' do
click_button 'Save changes'
page.within('.assignee') do
- expect(page).to have_content 'No assignee - assign yourself'
+ expect(page).to have_content 'None - assign yourself'
end
expect(issue.reload.assignees).to be_empty
@@ -233,8 +235,8 @@ describe 'Issues' do
created_at: Time.now - (index * 60))
end
end
- let(:newer_due_milestone) { create(:milestone, due_date: '2013-12-11') }
- let(:later_due_milestone) { create(:milestone, due_date: '2013-12-12') }
+ let(:newer_due_milestone) { create(:milestone, project: project, due_date: '2013-12-11') }
+ let(:later_due_milestone) { create(:milestone, project: project, due_date: '2013-12-12') }
it 'sorts by newest' do
visit project_issues_path(project, sort: sort_value_created_date)
@@ -465,7 +467,7 @@ describe 'Issues' do
click_link 'Edit'
click_link 'Unassigned'
first('.title').click
- expect(page).to have_content 'No assignee'
+ expect(page).to have_content 'None'
end
# wait_for_requests does not work with vue-resource at the moment
@@ -479,7 +481,7 @@ describe 'Issues' do
visit project_issue_path(project, issue2)
page.within('.assignee') do
- expect(page).to have_content "No assignee"
+ expect(page).to have_content "None"
end
page.within '.assignee' do
@@ -497,12 +499,21 @@ describe 'Issues' do
it 'allows user to unselect themselves', :js do
issue2 = create(:issue, project: project, author: user)
+
visit project_issue_path(project, issue2)
+ def close_dropdown_menu_if_visible
+ find('.dropdown-menu-toggle', visible: :all).tap do |toggle|
+ toggle.click if toggle.visible?
+ end
+ end
+
page.within '.assignee' do
click_link 'Edit'
click_link user.name
+ close_dropdown_menu_if_visible
+
page.within '.value .author' do
expect(page).to have_content user.name
end
@@ -510,8 +521,10 @@ describe 'Issues' do
click_link 'Edit'
click_link user.name
+ close_dropdown_menu_if_visible
+
page.within '.value .assign-yourself' do
- expect(page).to have_content "No assignee"
+ expect(page).to have_content "None"
end
end
end
@@ -764,10 +777,10 @@ describe 'Issues' do
wait_for_requests
- expect(page).to have_no_content 'No due date'
+ expect(page).to have_no_content 'None'
click_link 'remove due date'
- expect(page).to have_content 'No due date'
+ expect(page).to have_content 'None'
end
end
end
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
index 7c31e67a7fa..489651fea15 100644
--- a/spec/features/labels_hierarchy_spec.rb
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -21,7 +21,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do
end
shared_examples 'assigning labels from sidebar' do
- it 'can assign all ancestors labels' do
+ it 'can assign all ancestors labels', :quarantine do
[grandparent_group_label, parent_group_label, project_label_1].each do |label|
page.within('.block.labels') do
find('.edit-link').click
@@ -145,7 +145,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do
visit new_project_issue_path(project_1)
end
- it 'should be able to assign ancestor group labels' do
+ it 'is able to assign ancestor group labels' do
fill_in 'issue_title', with: 'new created issue'
fill_in 'issue_description', with: 'new issue description'
diff --git a/spec/features/markdown/copy_as_gfm_spec.rb b/spec/features/markdown/copy_as_gfm_spec.rb
index 60ddb02da2c..c30ac9c4ae2 100644
--- a/spec/features/markdown/copy_as_gfm_spec.rb
+++ b/spec/features/markdown/copy_as_gfm_spec.rb
@@ -55,15 +55,10 @@ describe 'Copy as GFM', :js do
To see how GitLab looks please see the [features page on our website](https://about.gitlab.com/features/).
* Manage Git repositories with fine grained access controls that keep your code secure
-
* Perform code reviews and enhance collaboration with merge requests
-
* Complete continuous integration (CI) and CD pipelines to builds, test, and deploy your applications
-
* Each project can also have an issue tracker, issue board, and a wiki
-
* Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises
-
* Completely free and open source (MIT Expat license)
GFM
)
@@ -116,13 +111,11 @@ describe 'Copy as GFM', :js do
<<~GFM,
* [ ] Unchecked task
-
* [x] Checked task
GFM
<<~GFM
1. [ ] Unchecked ordered task
-
1. [x] Checked ordered task
GFM
)
@@ -551,7 +544,6 @@ describe 'Copy as GFM', :js do
<<~GFM,
* List item
-
* List item 2
GFM
@@ -565,7 +557,6 @@ describe 'Copy as GFM', :js do
# nested lists
<<~GFM,
* Nested
-
* Lists
GFM
@@ -578,7 +569,6 @@ describe 'Copy as GFM', :js do
<<~GFM,
1. Ordered list item
-
1. Ordered list item 2
GFM
@@ -592,7 +582,6 @@ describe 'Copy as GFM', :js do
# nested ordered list
<<~GFM,
1. Nested
-
1. Ordered lists
GFM
diff --git a/spec/features/markdown/gitlab_flavored_markdown_spec.rb b/spec/features/markdown/gitlab_flavored_markdown_spec.rb
index 6997ca48427..8fda3c7193e 100644
--- a/spec/features/markdown/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/markdown/gitlab_flavored_markdown_spec.rb
@@ -20,8 +20,7 @@ describe "GitLab Flavored Markdown" do
let(:commit) { project.commit }
before do
- allow_any_instance_of(Commit).to receive(:title)
- .and_return("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details")
+ create_commit("fix #{issue.to_reference}\n\nask #{fred.to_reference} for details", project, user, 'master')
end
it "renders title in commits#index" do
diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb
index 7839b97122c..b35f985126c 100644
--- a/spec/features/merge_request/maintainer_edits_fork_spec.rb
+++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb
@@ -18,13 +18,15 @@ describe 'a maintainer edits files on a source-branch of an MR from a fork', :js
end
before do
+ stub_feature_flags(web_ide_default: false)
+
target_project.add_maintainer(user)
sign_in(user)
visit project_merge_request_path(target_project, merge_request)
click_link 'Changes'
wait_for_requests
- first('.js-file-title').click_link 'Edit'
+ first('.js-file-title').find('.js-edit-blob').click
wait_for_requests
end
diff --git a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
index 0ccab5b2fac..b8c4a78e24f 100644
--- a/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
+++ b/spec/features/merge_request/user_allows_commits_from_memebers_who_can_merge_spec.rb
@@ -76,7 +76,7 @@ describe 'create a merge request, allowing commits from members who can merge to
sign_in(member)
end
- it 'it hides the option from members' do
+ it 'hides the option from members' do
visit edit_project_merge_request_path(target_project, merge_request)
expect(page).not_to have_content('Allows commits from members who can merge to the target branch')
diff --git a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
index c837a6752f9..5db54f42264 100644
--- a/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_creates_image_diff_notes_spec.rb
@@ -113,7 +113,7 @@ describe 'Merge request > User creates image diff notes', :js do
create_image_diff_note
end
- it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes' do
+ it 'shows indicator and avatar badges, and allows collapsing/expanding the discussion notes', :quarantine do
indicator = find('.js-image-badge', match: :first)
badge = find('.user-avatar-link .badge', match: :first)
@@ -191,29 +191,119 @@ describe 'Merge request > User creates image diff notes', :js do
end
end
- describe 'image view modes' do
- before do
- visit project_commit_path(project, '2f63565e7aac07bcdadb654e253078b727143ec4')
+ shared_examples 'swipe view' do
+ it 'moves the swipe handle' do
+ # Simulate dragging swipe view slider
+ expect { drag_and_drop_by(find('.swipe-bar'), 20, 0) }
+ .to change { find('.swipe-bar')['style'] }
+ .from(a_string_matching('left: 1px'))
end
- it 'resizes image in onion skin view mode' do
- find('.view-modes-menu .onion-skin').click
+ it 'shows both images at the same position' do
+ drag_and_drop_by(find('.swipe-bar'), 40, 0)
- expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;')
+ expect(left_position('.frame.added img'))
+ .to eq(left_position('.frame.deleted img'))
end
+ end
- it 'resets onion skin view mode opacity when toggling between view modes' do
- find('.view-modes-menu .onion-skin').click
-
+ shared_examples 'onion skin' do
+ it 'resets opacity when toggling between view modes' do
# Simulate dragging onion-skin slider
drag_and_drop_by(find('.dragger'), -30, 0)
expect(find('.onion-skin-frame .frame.added', visible: false)['style']).not_to match('opacity: 1;')
+ switch_to_swipe_view
+ switch_to_onion_skin
+
+ expect(find('.onion-skin-frame .frame.added', visible: false)['style']).to match('opacity: 1;')
+ end
+ end
+
+ describe 'changes tab image diff' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, target_branch: 'master', source_branch: 'deleted-image-test', author: user) }
+
+ before do
+ visit diffs_project_merge_request_path(project, merge_request)
+ click_link "Changes"
+ end
+
+ def set_image_diff_sources
+ # set path of added and deleted images to something the spec can view
+ page.execute_script("document.querySelector('.frame.added img').src = '/apple-touch-icon.png';")
+ page.execute_script("document.querySelector('.frame.deleted img').src = '/favicon.png';")
+
+ wait_for_requests
+
+ expect(find('.frame.added img', visible: false)['src']).to match('/apple-touch-icon.png')
+ expect(find('.frame.deleted img', visible: false)['src']).to match('/favicon.png')
+ end
+
+ def switch_to_swipe_view
+ # it isn't given the .swipe class in the merge request diff
+ find('.view-modes-menu li:nth-child(2)').click
+ expect(find('.view-modes-menu li.active')).to have_content('Swipe')
+
+ set_image_diff_sources
+ end
+
+ def switch_to_onion_skin
+ # it isn't given the .onion-skin class in the merge request diff
+ find('.view-modes-menu li:nth-child(3)').click
+ expect(find('.view-modes-menu li.active')).to have_content('Onion skin')
+
+ set_image_diff_sources
+ end
+
+ describe 'onion skin' do
+ before do
+ switch_to_onion_skin
+ end
+
+ it_behaves_like 'onion skin'
+ end
+
+ describe 'swipe view' do
+ before do
+ switch_to_swipe_view
+ end
+
+ it_behaves_like 'swipe view'
+ end
+ end
+
+ describe 'image view modes' do
+ before do
+ visit project_commit_path(project, '2f63565e7aac07bcdadb654e253078b727143ec4')
+ end
+
+ def switch_to_swipe_view
find('.view-modes-menu .swipe').click
+ end
+
+ def switch_to_onion_skin
find('.view-modes-menu .onion-skin').click
+ end
- expect(find('.onion-skin-frame .frame.added', visible: false)['style']).to match('opacity: 1;')
+ describe 'onion skin' do
+ before do
+ switch_to_onion_skin
+ end
+
+ it 'resizes image' do
+ expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;')
+ end
+
+ it_behaves_like 'onion skin'
+ end
+
+ describe 'swipe view' do
+ before do
+ switch_to_swipe_view
+ end
+
+ it_behaves_like 'swipe view'
end
end
@@ -232,4 +322,8 @@ describe 'Merge request > User creates image diff notes', :js do
click_button 'Comment'
wait_for_requests
end
+
+ def left_position(element)
+ page.evaluate_script("document.querySelectorAll('#{element}')[0].getBoundingClientRect().left;")
+ end
end
diff --git a/spec/features/merge_request/user_creates_merge_request_spec.rb b/spec/features/merge_request/user_creates_merge_request_spec.rb
index ea2bb1503bb..d05ef2a8f12 100644
--- a/spec/features/merge_request/user_creates_merge_request_spec.rb
+++ b/spec/features/merge_request/user_creates_merge_request_spec.rb
@@ -8,8 +8,6 @@ describe "User creates a merge request", :js do
let(:user) { create(:user) }
before do
- stub_feature_flags(approval_rules: false)
-
project.add_maintainer(user)
sign_in(user)
end
@@ -68,15 +66,15 @@ describe "User creates a merge request", :js do
fill_in("Title", with: title)
end
- click_button("Assignee")
-
expect(find(".js-assignee-search")["data-project-id"]).to eq(project.id.to_s)
+ find('.js-assignee-search').click
page.within(".dropdown-menu-user") do
expect(page).to have_content("Unassigned")
.and have_content(user.name)
.and have_content(project.users.first.name)
end
+ find('.js-assignee-search').click
click_button("Submit merge request")
diff --git a/spec/features/merge_request/user_creates_mr_spec.rb b/spec/features/merge_request/user_creates_mr_spec.rb
index c169a68cd1c..c9dedab048a 100644
--- a/spec/features/merge_request/user_creates_mr_spec.rb
+++ b/spec/features/merge_request/user_creates_mr_spec.rb
@@ -1,11 +1,18 @@
require 'rails_helper'
describe 'Merge request > User creates MR' do
- it_behaves_like 'a creatable merge request'
+ include ProjectForksHelper
- context 'from a forked project' do
- include ProjectForksHelper
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+ context 'non-fork merge request' do
+ include_context 'merge request create context'
+ it_behaves_like 'a creatable merge request'
+ end
+
+ context 'from a forked project' do
let(:canonical_project) { create(:project, :public, :repository) }
let(:source_project) do
@@ -15,6 +22,7 @@ describe 'Merge request > User creates MR' do
end
context 'to canonical project' do
+ include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
@@ -25,6 +33,7 @@ describe 'Merge request > User creates MR' do
namespace: user.namespace)
end
+ include_context 'merge request create context'
it_behaves_like 'a creatable merge request'
end
end
diff --git a/spec/features/merge_request/user_edits_mr_spec.rb b/spec/features/merge_request/user_edits_mr_spec.rb
index 3152707136c..25979513ead 100644
--- a/spec/features/merge_request/user_edits_mr_spec.rb
+++ b/spec/features/merge_request/user_edits_mr_spec.rb
@@ -1,13 +1,21 @@
-require 'rails_helper'
+require 'spec_helper'
describe 'Merge request > User edits MR' do
include ProjectForksHelper
- it_behaves_like 'an editable merge request'
+ before do
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ context 'non-fork merge request' do
+ include_context 'merge request edit context'
+ it_behaves_like 'an editable merge request'
+ end
context 'for a forked project' do
- it_behaves_like 'an editable merge request' do
- let(:source_project) { fork_project(target_project, nil, repository: true) }
- end
+ let(:source_project) { fork_project(target_project, nil, repository: true) }
+
+ include_context 'merge request edit context'
+ it_behaves_like 'an editable merge request'
end
end
diff --git a/spec/features/merge_request/user_merges_merge_request_spec.rb b/spec/features/merge_request/user_merges_merge_request_spec.rb
index 6539e6e9208..da15a4bda4b 100644
--- a/spec/features/merge_request/user_merges_merge_request_spec.rb
+++ b/spec/features/merge_request/user_merges_merge_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe "User merges a merge request", :js do
diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb
index 6e54aa6006b..586b3ba170d 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
@@ -52,7 +52,7 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# so we have to wait for asynchronous call to reload it
# and have_content expectation handles that.
#
- expect(page).to have_content "Pipeline ##{pipeline.id} running"
+ expect(page).to have_content "Pipeline ##{pipeline.id} (##{pipeline.iid}) running"
end
it_behaves_like 'Merge when pipeline succeeds activator'
@@ -74,11 +74,12 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
source_project: project,
title: 'Bug NS-04',
author: user,
- merge_user: user,
- merge_params: { force_remove_source_branch: '1' })
+ merge_user: user)
end
before do
+ merge_request.merge_params['force_remove_source_branch'] = '0'
+ merge_request.save!
click_link "Cancel automatic merge"
end
@@ -102,11 +103,11 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
context 'when merge when pipeline succeeds is enabled' do
let(:merge_request) do
- create(:merge_request_with_diffs, :simple, source_project: project,
- author: user,
- merge_user: user,
- title: 'MepMep',
- merge_when_pipeline_succeeds: true)
+ create(:merge_request_with_diffs, :simple, :merge_when_pipeline_succeeds,
+ source_project: project,
+ author: user,
+ merge_user: user,
+ title: 'MepMep')
end
let!(:build) do
create(:ci_build, pipeline: pipeline)
@@ -158,8 +159,8 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
@@ -177,8 +178,8 @@ describe 'Merge request > User merges when pipeline succeeds', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
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 51b78d3e7d1..19edce1b562 100644
--- a/spec/features/merge_request/user_posts_diff_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb
@@ -178,7 +178,7 @@ describe 'Merge request > User posts diff notes', :js do
end
end
- describe 'with muliple note forms' do
+ describe 'with multiple note forms' do
before do
visit diffs_project_merge_request_path(project, merge_request, view: 'inline')
click_diff_line(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 1bbcf455ac7..e5770905dbd 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -67,20 +67,8 @@ describe 'Merge request > User posts notes', :js do
end
end
- describe 'when reply_to_individual_notes feature flag is not set' do
+ describe 'reply button' do
before do
- stub_feature_flags(reply_to_individual_notes: false)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not show a reply button' do
- expect(page).to have_no_selector('.js-reply-button')
- end
- end
-
- describe 'when reply_to_individual_notes feature flag is set' do
- before do
- stub_feature_flags(reply_to_individual_notes: true)
visit project_merge_request_path(project, merge_request)
end
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 16c058ab6bd..8fd44b87e5a 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -164,6 +164,21 @@ describe 'Merge request > User resolves conflicts', :js do
expect(page).to have_content('Gregor Samsa woke from troubled dreams')
end
end
+
+ context "with malicious branch name" do
+ let(:bad_branch_name) { "malicious-branch-{{toString.constructor('alert(/xss/)')()}}" }
+ let(:branch) { project.repository.create_branch(bad_branch_name, 'conflict-resolvable') }
+ let(:merge_request) { create_merge_request(branch.name) }
+
+ before do
+ visit project_merge_request_path(project, merge_request)
+ click_link('conflicts', href: %r{/conflicts\Z})
+ end
+
+ it "renders bad name without xss issues" do
+ expect(find('.resolve-conflicts-form .resolve-info')).to have_content(bad_branch_name)
+ end
+ end
end
UNRESOLVABLE_CONFLICTS = {
diff --git a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
index 8c2599615cb..2f7d359575e 100644
--- a/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
+++ b/spec/features/merge_request/user_scrolls_to_note_on_load_spec.rb
@@ -5,9 +5,7 @@ describe 'Merge request > User scrolls to note on load', :js do
let(:user) { project.creator }
let(:merge_request) { create(:merge_request, source_project: project, author: user) }
let(:note) { create(:diff_note_on_merge_request, noteable: merge_request, project: project) }
- let(:resolved_note) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
let(:fragment_id) { "#note_#{note.id}" }
- let(:collapsed_fragment_id) { "#note_#{resolved_note.id}" }
before do
sign_in(user)
@@ -45,13 +43,35 @@ describe 'Merge request > User scrolls to note on load', :js do
end
end
- # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
- xit 'expands collapsed notes' do
- visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}"
- note_element = find(collapsed_fragment_id)
- note_container = note_element.ancestor('.timeline-content')
+ context 'resolved notes' do
+ let(:collapsed_fragment_id) { "#note_#{resolved_note.id}" }
- expect(note_element.visible?).to eq true
- expect(note_container.find('.line_content.noteable_line.old', match: :first).visible?).to eq true
+ context 'when diff note' do
+ let(:resolved_note) { create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+
+ it 'expands collapsed notes' do
+ visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}"
+
+ note_element = find(collapsed_fragment_id)
+ diff_container = note_element.ancestor('.diff-content')
+
+ expect(note_element.visible?).to eq(true)
+ expect(diff_container.visible?).to eq(true)
+ end
+ end
+
+ context 'when non-diff note' do
+ let(:non_diff_discussion) { create(:discussion_note_on_merge_request, :resolved, noteable: merge_request, project: project) }
+ let(:resolved_note) { create(:discussion_note_on_merge_request, :resolved, noteable: merge_request, project: project, in_reply_to: non_diff_discussion) }
+
+ it 'expands collapsed replies' do
+ visit "#{project_merge_request_path(project, merge_request)}#{collapsed_fragment_id}"
+
+ note_element = find(collapsed_fragment_id)
+
+ expect(note_element.visible?).to eq(true)
+ expect(note_element.sibling('.replies-toggle')[:class]).to include('expanded')
+ end
+ end
end
end
diff --git a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
index 7b473faa884..28f88718ec1 100644
--- a/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_request_pipelines_spec.rb
@@ -2,7 +2,7 @@
require 'rails_helper'
-describe 'Merge request > User sees merge request pipelines', :js do
+describe 'Merge request > User sees pipelines triggered by merge request', :js do
include ProjectForksHelper
include TestReportsHelper
@@ -47,9 +47,9 @@ describe 'Merge request > User sees merge request pipelines', :js do
.execute(:push)
end
- let!(:merge_request_pipeline) do
+ let!(:detached_merge_request_pipeline) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
- .execute(:merge_request, merge_request: merge_request)
+ .execute(:merge_request_event, merge_request: merge_request)
end
before do
@@ -60,16 +60,16 @@ describe 'Merge request > User sees merge request pipelines', :js do
end
end
- it 'sees branch pipelines and merge request pipelines in correct order' do
+ it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 2)
- expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}")
+ expect(first('.js-pipeline-url-link')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
- it 'sees the latest merge request pipeline as the head pipeline' do
+ it 'sees the latest detached merge request pipeline as the head pipeline' do
page.within('.ci-widget-content') do
- expect(page).to have_content("##{merge_request_pipeline.id}")
+ expect(page).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -79,9 +79,9 @@ describe 'Merge request > User sees merge request pipelines', :js do
.execute(:push)
end
- let!(:merge_request_pipeline_2) do
+ let!(:detached_merge_request_pipeline_2) do
Ci::CreatePipelineService.new(project, user, ref: 'feature')
- .execute(:merge_request, merge_request: merge_request)
+ .execute(:merge_request_event, merge_request: merge_request)
end
before do
@@ -92,15 +92,15 @@ describe 'Merge request > User sees merge request pipelines', :js do
end
end
- it 'sees branch pipelines and merge request pipelines in correct order' do
+ it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 4)
expect(all('.js-pipeline-url-link')[0])
- .to have_content("##{merge_request_pipeline_2.id}")
+ .to have_content("##{detached_merge_request_pipeline_2.id}")
expect(all('.js-pipeline-url-link')[1])
- .to have_content("##{merge_request_pipeline.id}")
+ .to have_content("##{detached_merge_request_pipeline.id}")
expect(all('.js-pipeline-url-link')[2])
.to have_content("##{push_pipeline_2.id}")
@@ -110,25 +110,25 @@ describe 'Merge request > User sees merge request pipelines', :js do
end
end
- it 'sees merge request tag for merge request pipelines' do
+ it 'sees detached tag for detached merge request pipelines' do
page.within('.ci-table') do
expect(all('.pipeline-tags')[0])
- .to have_content("merge request")
+ .to have_content("detached")
expect(all('.pipeline-tags')[1])
- .to have_content("merge request")
+ .to have_content("detached")
expect(all('.pipeline-tags')[2])
- .not_to have_content("merge request")
+ .not_to have_content("detached")
expect(all('.pipeline-tags')[3])
- .not_to have_content("merge request")
+ .not_to have_content("detached")
end
end
- it 'sees the latest merge request pipeline as the head pipeline' do
+ it 'sees the latest detached merge request pipeline as the head pipeline' do
page.within('.ci-widget-content') do
- expect(page).to have_content("##{merge_request_pipeline_2.id}")
+ expect(page).to have_content("##{detached_merge_request_pipeline_2.id}")
end
end
end
@@ -140,16 +140,16 @@ describe 'Merge request > User sees merge request pipelines', :js do
wait_for_requests
end
- context 'when merge request pipeline is pending' do
+ context 'when detached merge request pipeline is pending' do
it 'waits the head pipeline' do
expect(page).to have_content('to be merged automatically when the pipeline succeeds')
expect(page).to have_link('Cancel automatic merge')
end
end
- context 'when merge request pipeline succeeds' do
+ context 'when detached merge request pipeline succeeds' do
before do
- merge_request_pipeline.succeed!
+ detached_merge_request_pipeline.succeed!
wait_for_requests
end
@@ -218,9 +218,9 @@ describe 'Merge request > User sees merge request pipelines', :js do
.execute(:push)
end
- let!(:merge_request_pipeline) do
+ let!(:detached_merge_request_pipeline) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
- .execute(:merge_request, merge_request: merge_request)
+ .execute(:merge_request_event, merge_request: merge_request)
end
let(:forked_project) { fork_project(project, user2, repository: true) }
@@ -236,16 +236,16 @@ describe 'Merge request > User sees merge request pipelines', :js do
end
end
- it 'sees branch pipelines and merge request pipelines in correct order' do
+ it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 2)
- expect(first('.js-pipeline-url-link')).to have_content("##{merge_request_pipeline.id}")
+ expect(first('.js-pipeline-url-link')).to have_content("##{detached_merge_request_pipeline.id}")
end
end
- it 'sees the latest merge request pipeline as the head pipeline' do
+ it 'sees the latest detached merge request pipeline as the head pipeline' do
page.within('.ci-widget-content') do
- expect(page).to have_content("##{merge_request_pipeline.id}")
+ expect(page).to have_content("##{detached_merge_request_pipeline.id}")
end
end
@@ -261,9 +261,9 @@ describe 'Merge request > User sees merge request pipelines', :js do
.execute(:push)
end
- let!(:merge_request_pipeline_2) do
+ let!(:detached_merge_request_pipeline_2) do
Ci::CreatePipelineService.new(forked_project, user2, ref: 'feature')
- .execute(:merge_request, merge_request: merge_request)
+ .execute(:merge_request_event, merge_request: merge_request)
end
before do
@@ -274,15 +274,15 @@ describe 'Merge request > User sees merge request pipelines', :js do
end
end
- it 'sees branch pipelines and merge request pipelines in correct order' do
+ it 'sees branch pipelines and detached merge request pipelines in correct order' do
page.within('.ci-table') do
expect(page).to have_selector('.ci-pending', count: 4)
expect(all('.js-pipeline-url-link')[0])
- .to have_content("##{merge_request_pipeline_2.id}")
+ .to have_content("##{detached_merge_request_pipeline_2.id}")
expect(all('.js-pipeline-url-link')[1])
- .to have_content("##{merge_request_pipeline.id}")
+ .to have_content("##{detached_merge_request_pipeline.id}")
expect(all('.js-pipeline-url-link')[2])
.to have_content("##{push_pipeline_2.id}")
@@ -292,25 +292,25 @@ describe 'Merge request > User sees merge request pipelines', :js do
end
end
- it 'sees merge request tag for merge request pipelines' do
+ it 'sees detached tag for detached merge request pipelines' do
page.within('.ci-table') do
expect(all('.pipeline-tags')[0])
- .to have_content("merge request")
+ .to have_content("detached")
expect(all('.pipeline-tags')[1])
- .to have_content("merge request")
+ .to have_content("detached")
expect(all('.pipeline-tags')[2])
- .not_to have_content("merge request")
+ .not_to have_content("detached")
expect(all('.pipeline-tags')[3])
- .not_to have_content("merge request")
+ .not_to have_content("detached")
end
end
- it 'sees the latest merge request pipeline as the head pipeline' do
+ it 'sees the latest detached merge request pipeline as the head pipeline' do
page.within('.ci-widget-content') do
- expect(page).to have_content("##{merge_request_pipeline_2.id}")
+ expect(page).to have_content("##{detached_merge_request_pipeline_2.id}")
end
end
@@ -328,16 +328,16 @@ describe 'Merge request > User sees merge request pipelines', :js do
wait_for_requests
end
- context 'when merge request pipeline is pending' do
+ context 'when detached merge request pipeline is pending' do
it 'waits the head pipeline' do
expect(page).to have_content('to be merged automatically when the pipeline succeeds')
expect(page).to have_link('Cancel automatic merge')
end
end
- context 'when merge request pipeline succeeds' do
+ context 'when detached merge request pipeline succeeds' do
before do
- merge_request_pipeline.succeed!
+ detached_merge_request_pipeline.succeed!
wait_for_requests
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index afb978d7c45..0066e985fbb 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -145,6 +145,119 @@ describe 'Merge request > User sees merge widget', :js do
end
end
+ context 'when merge request has a branch pipeline as the head pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ ref: merge_request.source_branch,
+ sha: merge_request.source_branch_sha,
+ project: merge_request.source_project)
+ end
+
+ before do
+ merge_request.update_head_pipeline
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'shows head pipeline information' do
+ within '.ci-widget-content' do
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
+ "for #{pipeline.short_sha} " \
+ "on #{pipeline.ref}")
+ end
+ end
+ end
+
+ context 'when merge request has a detached merge request pipeline as the head pipeline' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_detached_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project)
+ end
+
+ let!(:pipeline) do
+ merge_request.all_pipelines.last
+ end
+
+ let(:source_project) { project }
+ let(:target_project) { project }
+
+ before do
+ merge_request.update_head_pipeline
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'shows head pipeline information' do
+ within '.ci-widget-content' do
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
+ "for #{pipeline.short_sha} " \
+ "on #{merge_request.to_reference} " \
+ "with #{merge_request.source_branch}")
+ end
+ end
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+
+ it 'shows head pipeline information' do
+ within '.ci-widget-content' do
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
+ "for #{pipeline.short_sha} " \
+ "on #{merge_request.to_reference} " \
+ "with #{merge_request.source_branch}")
+ end
+ end
+ end
+ end
+
+ context 'when merge request has a merge request pipeline as the head pipeline' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project,
+ merge_sha: merge_sha)
+ end
+
+ let!(:pipeline) do
+ merge_request.all_pipelines.last
+ end
+
+ let(:source_project) { project }
+ let(:target_project) { project }
+ let(:merge_sha) { project.commit.sha }
+
+ before do
+ merge_request.update_head_pipeline
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'shows head pipeline information' do
+ within '.ci-widget-content' do
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
+ "for #{pipeline.short_sha} " \
+ "on #{merge_request.to_reference} " \
+ "with #{merge_request.source_branch} " \
+ "into #{merge_request.target_branch}")
+ end
+ end
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+ let(:merge_sha) { source_project.commit.sha }
+
+ it 'shows head pipeline information' do
+ within '.ci-widget-content' do
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) pending " \
+ "for #{pipeline.short_sha} " \
+ "on #{merge_request.to_reference} " \
+ "with #{merge_request.source_branch} " \
+ "into #{merge_request.target_branch}")
+ end
+ end
+ end
+ end
+
context 'view merge request with MWBS button' do
before do
commit_status = create(:commit_status, project: project, status: 'pending')
@@ -189,7 +302,7 @@ describe 'Merge request > User sees merge widget', :js do
visit project_merge_request_path(project_only_mwps, merge_request_in_only_mwps_project)
end
- it 'should be allowed to merge' do
+ it 'is allowed to merge' do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
@@ -201,7 +314,8 @@ describe 'Merge request > User sees merge widget', :js do
context 'view merge request with MWPS enabled but automatically merge fails' do
before do
merge_request.update(
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: merge_request.author,
merge_error: 'Something went wrong'
)
@@ -213,8 +327,8 @@ describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
@@ -234,8 +348,8 @@ describe 'Merge request > User sees merge widget', :js do
# Wait for the `ci_status` and `merge_check` requests
wait_for_requests
- page.within('.mr-widget-body') do
- expect(page).to have_content('Something went wrong')
+ page.within('.mr-section-container') do
+ expect(page).to have_content('Merge failed: Something went wrong')
end
end
end
@@ -557,4 +671,26 @@ describe 'Merge request > User sees merge widget', :js do
end
end
end
+
+ context 'when MR has pipeline but user does not have permission' do
+ let(:sha) { project.commit(merge_request.source_branch).sha }
+ let!(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
+
+ before do
+ project.update(
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC,
+ public_builds: false
+ )
+ merge_request.update!(head_pipeline: pipeline)
+ sign_out(:user)
+
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'renders a CI pipeline error' do
+ within '.ci-widget' do
+ expect(page).to have_content('Could not retrieve the pipeline status.')
+ end
+ end
+ end
end
diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
index 5188dc3625f..dd8900a3698 100644
--- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
@@ -27,7 +27,8 @@ describe 'Merge request < User sees mini pipeline graph', :js do
let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
before do
- create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file1)
+ job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
+ create(:ci_job_artifact, :archive, file: artifacts_file1, job: job)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
end
@@ -35,7 +36,8 @@ describe 'Merge request < User sees mini pipeline graph', :js do
xit 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
- create(:ci_build, :success, :trace_artifact, pipeline: pipeline, legacy_artifacts_file: artifacts_file2)
+ job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
+ create(:ci_job_artifact, :archive, file: artifacts_file2, job: job)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
diff --git a/spec/features/merge_request/user_sees_versions_spec.rb b/spec/features/merge_request/user_sees_versions_spec.rb
index aa91ade46ca..6eae3fd4676 100644
--- a/spec/features/merge_request/user_sees_versions_spec.rb
+++ b/spec/features/merge_request/user_sees_versions_spec.rb
@@ -1,7 +1,11 @@
require 'rails_helper'
describe 'Merge request > User sees versions', :js do
- let(:merge_request) { create(:merge_request, importing: true) }
+ let(:merge_request) do
+ create(:merge_request).tap do |mr|
+ mr.merge_request_diff.destroy
+ end
+ end
let(:project) { merge_request.source_project }
let(:user) { project.creator }
let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
@@ -226,7 +230,7 @@ describe 'Merge request > User sees versions', :js do
wait_for_requests
end
- it 'should only show diffs from the commit' do
+ it 'only shows diffs from the commit' do
diff_commit_ids = find_all('.diff-file [data-commit-id]').map {|diff| diff['data-commit-id']}
expect(diff_commit_ids).not_to be_empty
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index c19e299097e..04c7f4b6c76 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -6,6 +6,14 @@ describe 'User comments on a diff', :js do
include MergeRequestDiffHelpers
include RepoHelpers
+ def expect_suggestion_has_content(element, expected_changing_content, expected_suggested_content)
+ changing_content = element.all(:css, '.line_holder.old').map(&:text)
+ suggested_content = element.all(:css, '.line_holder.new').map(&:text)
+
+ expect(changing_content).to eq(expected_changing_content)
+ expect(suggested_content).to eq(expected_suggested_content)
+ end
+
let(:project) { create(:project, :repository) }
let(:merge_request) do
create(:merge_request_with_diffs, source_project: project, target_project: project, source_branch: 'merge-test')
@@ -33,8 +41,18 @@ describe 'User comments on a diff', :js do
page.within('.diff-discussions') do
expect(page).to have_button('Apply suggestion')
expect(page).to have_content('Suggested change')
- expect(page).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git')
- expect(page).to have_content('# change to a comment')
+ end
+
+ page.within('.md-suggestion-diff') do
+ expected_changing_content = [
+ "6 url = https://github.com/gitlabhq/gitlab-shell.git"
+ ]
+
+ expected_suggested_content = [
+ "6 # change to a comment"
+ ]
+
+ expect_suggestion_has_content(page, expected_changing_content, expected_suggested_content)
end
end
@@ -64,7 +82,7 @@ describe 'User comments on a diff', :js do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
- fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion\n# or that\n```")
+ fill_in('note_note', with: "```suggestion\n# change to a comment\n```\n```suggestion:-2\n# or that\n# heh\n```")
click_button('Comment')
end
@@ -74,11 +92,94 @@ describe 'User comments on a diff', :js do
suggestion_1 = page.all(:css, '.md-suggestion-diff')[0]
suggestion_2 = page.all(:css, '.md-suggestion-diff')[1]
- expect(suggestion_1).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git')
- expect(suggestion_1).to have_content('# change to a comment')
+ suggestion_1_expected_changing_content = [
+ "6 url = https://github.com/gitlabhq/gitlab-shell.git"
+ ]
+ suggestion_1_expected_suggested_content = [
+ "6 # change to a comment"
+ ]
+
+ suggestion_2_expected_changing_content = [
+ "4 [submodule \"gitlab-shell\"]",
+ "5 path = gitlab-shell",
+ "6 url = https://github.com/gitlabhq/gitlab-shell.git"
+ ]
+ suggestion_2_expected_suggested_content = [
+ "4 # or that",
+ "5 # heh"
+ ]
+
+ expect_suggestion_has_content(suggestion_1,
+ suggestion_1_expected_changing_content,
+ suggestion_1_expected_suggested_content)
+
+ expect_suggestion_has_content(suggestion_2,
+ suggestion_2_expected_changing_content,
+ suggestion_2_expected_suggested_content)
+ end
+ end
+ end
+
+ context 'multi-line suggestions' do
+ before do
+ click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+
+ page.within('.js-discussion-note-form') do
+ fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
+ click_button('Comment')
+ end
+
+ wait_for_requests
+ end
+
+ it 'suggestion is presented' do
+ page.within('.diff-discussions') do
+ expect(page).to have_button('Apply suggestion')
+ expect(page).to have_content('Suggested change')
+ end
+
+ page.within('.md-suggestion-diff') do
+ expected_changing_content = [
+ "3 url = git://github.com/randx/six.git",
+ "4 [submodule \"gitlab-shell\"]",
+ "5 path = gitlab-shell",
+ "6 url = https://github.com/gitlabhq/gitlab-shell.git",
+ "7 [submodule \"gitlab-grack\"]",
+ "8 path = gitlab-grack",
+ "9 url = https://gitlab.com/gitlab-org/gitlab-grack.git"
+ ]
+
+ expected_suggested_content = [
+ "3 # change to a",
+ "4 # comment",
+ "5 # with",
+ "6 # broken",
+ "7 # lines"
+ ]
+
+ expect_suggestion_has_content(page, expected_changing_content, expected_suggested_content)
+ end
+ end
+
+ it 'suggestion is appliable' do
+ page.within('.diff-discussions') do
+ expect(page).not_to have_content('Applied')
+
+ click_button('Apply suggestion')
+ wait_for_requests
+
+ expect(page).to have_content('Applied')
+ end
+ end
+
+ it 'resolves discussion when applied' do
+ page.within('.diff-discussions') do
+ expect(page).not_to have_content('Unresolve discussion')
+
+ click_button('Apply suggestion')
+ wait_for_requests
- expect(suggestion_2).to have_content(' url = https://github.com/gitlabhq/gitlab-shell.git')
- expect(suggestion_2).to have_content('# or that')
+ expect(page).to have_content('Unresolve discussion')
end
end
end
diff --git a/spec/features/merge_request/user_uses_quick_actions_spec.rb b/spec/features/merge_request/user_uses_quick_actions_spec.rb
index b81478a481f..988a8302527 100644
--- a/spec/features/merge_request/user_uses_quick_actions_spec.rb
+++ b/spec/features/merge_request/user_uses_quick_actions_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe 'Merge request > User uses quick actions', :js do
@@ -9,9 +11,41 @@ describe 'Merge request > User uses quick actions', :js do
let(:merge_request) { create(:merge_request, source_project: project) }
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
- it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do
+ context "issuable common quick actions" do
+ let!(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
+ let(:maintainer) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let!(:label_bug) { create(:label, project: project, title: 'bug') }
+ let!(:label_feature) { create(:label, project: project, title: 'feature') }
+ let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
let(:issuable) { create(:merge_request, source_project: project) }
- let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } }
+ let(:source_issuable) { create(:issue, project: project, milestone: milestone, labels: [label_bug, label_feature])}
+
+ it_behaves_like 'assign quick action', :merge_request
+ it_behaves_like 'unassign quick action', :merge_request
+ it_behaves_like 'close quick action', :merge_request
+ it_behaves_like 'reopen quick action', :merge_request
+ it_behaves_like 'title quick action', :merge_request
+ it_behaves_like 'todo quick action', :merge_request
+ it_behaves_like 'done quick action', :merge_request
+ it_behaves_like 'subscribe quick action', :merge_request
+ it_behaves_like 'unsubscribe quick action', :merge_request
+ it_behaves_like 'lock quick action', :merge_request
+ it_behaves_like 'unlock quick action', :merge_request
+ it_behaves_like 'milestone quick action', :merge_request
+ it_behaves_like 'remove_milestone quick action', :merge_request
+ it_behaves_like 'label quick action', :merge_request
+ it_behaves_like 'unlabel quick action', :merge_request
+ it_behaves_like 'relabel quick action', :merge_request
+ it_behaves_like 'award quick action', :merge_request
+ it_behaves_like 'estimate quick action', :merge_request
+ it_behaves_like 'remove_estimate quick action', :merge_request
+ it_behaves_like 'spend quick action', :merge_request
+ it_behaves_like 'remove_time_spent quick action', :merge_request
+ it_behaves_like 'shrug quick action', :merge_request
+ it_behaves_like 'tableflip quick action', :merge_request
+ it_behaves_like 'copy_metadata quick action', :merge_request
+ it_behaves_like 'issuable time tracker', :merge_request
end
describe 'merge-request-only commands' do
@@ -24,200 +58,8 @@ describe 'Merge request > User uses quick actions', :js do
project.add_maintainer(user)
end
- describe 'time tracking' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it_behaves_like 'issuable time tracker'
- end
-
- describe 'toggling the WIP prefix in the title from note' do
- context 'when the current user can toggle the WIP prefix' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'adds the WIP: prefix to the title' do
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- expect(page).to have_content 'Commands applied'
-
- expect(merge_request.reload.work_in_progress?).to eq true
- end
-
- it 'removes the WIP: prefix from the title' do
- merge_request.title = merge_request.wip_title
- merge_request.save
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- expect(page).to have_content 'Commands applied'
-
- expect(merge_request.reload.work_in_progress?).to eq false
- end
- end
-
- context 'when the current user cannot toggle the WIP prefix' do
- before do
- project.add_guest(guest)
- sign_in(guest)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not change the WIP prefix' do
- add_note("/wip")
-
- expect(page).not_to have_content '/wip'
- expect(page).not_to have_content 'Commands applied'
-
- expect(merge_request.reload.work_in_progress?).to eq false
- end
- end
- end
-
- describe 'merging the MR from the note' do
- context 'when the current user can merge the MR' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'merges the MR' do
- add_note("/merge")
-
- expect(page).to have_content 'Commands applied'
-
- expect(merge_request.reload).to be_merged
- end
- end
-
- context 'when the head diff changes in the meanwhile' do
- before do
- merge_request.source_branch = 'another_branch'
- merge_request.save
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not merge the MR' do
- add_note("/merge")
-
- expect(page).not_to have_content 'Your commands have been executed!'
-
- expect(merge_request.reload).not_to be_merged
- end
- end
-
- context 'when the current user cannot merge the MR' do
- before do
- project.add_guest(guest)
- sign_in(guest)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not merge the MR' do
- add_note("/merge")
-
- expect(page).not_to have_content 'Your commands have been executed!'
-
- expect(merge_request.reload).not_to be_merged
- end
- end
- end
-
- describe 'adding a due date from note' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not recognize the command nor create a note' do
- add_note('/due 2016-08-28')
-
- expect(page).not_to have_content '/due 2016-08-28'
- end
- end
-
- describe '/target_branch command in merge request' do
- let(:another_project) { create(:project, :public, :repository) }
- let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
-
- before do
- another_project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'changes target_branch in new merge_request' do
- visit project_new_merge_request_path(another_project, new_url_opts)
-
- fill_in "merge_request_title", with: 'My brand new feature'
- fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
- click_button "Submit merge request"
-
- merge_request = another_project.merge_requests.first
- expect(merge_request.description).to eq "le feature \nFeature description:"
- expect(merge_request.target_branch).to eq 'fix'
- end
-
- it 'does not change target branch when merge request is edited' do
- new_merge_request = create(:merge_request, source_project: another_project)
-
- visit edit_project_merge_request_path(another_project, new_merge_request)
- fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n"
- click_button "Save changes"
-
- new_merge_request = another_project.merge_requests.first
- expect(new_merge_request.description).to include('/target_branch')
- expect(new_merge_request.target_branch).not_to eq('fix')
- end
- end
-
- describe '/target_branch command from note' do
- context 'when the current user can change target branch' do
- before do
- sign_in(user)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'changes target branch from a note' do
- add_note("message start \n/target_branch merge-test\n message end.")
-
- wait_for_requests
- expect(page).not_to have_content('/target_branch')
- expect(page).to have_content('message start')
- expect(page).to have_content('message end.')
-
- expect(merge_request.reload.target_branch).to eq 'merge-test'
- end
-
- it 'does not fail when target branch does not exists' do
- add_note('/target_branch totally_not_existing_branch')
-
- expect(page).not_to have_content('/target_branch')
-
- expect(merge_request.target_branch).to eq 'feature'
- end
- end
-
- context 'when current user can not change target branch' do
- before do
- project.add_guest(guest)
- sign_in(guest)
- visit project_merge_request_path(project, merge_request)
- end
-
- it 'does not change target branch' do
- add_note('/target_branch merge-test')
-
- expect(page).not_to have_content '/target_branch merge-test'
-
- expect(merge_request.target_branch).to eq 'feature'
- end
- end
- end
+ it_behaves_like 'merge quick action'
+ it_behaves_like 'target_branch quick action'
+ it_behaves_like 'wip quick action'
end
end
diff --git a/spec/features/merge_request/user_views_diffs_spec.rb b/spec/features/merge_request/user_views_diffs_spec.rb
index 0434db04113..74342b16cb2 100644
--- a/spec/features/merge_request/user_views_diffs_spec.rb
+++ b/spec/features/merge_request/user_views_diffs_spec.rb
@@ -34,6 +34,16 @@ describe 'User views diffs', :js do
expect(page).not_to have_selector('.mr-loading-status .loading', visible: true)
end
+ it 'expands all diffs' do
+ first('#a5cc2925ca8258af241be7e5b0381edf30266302 .js-file-title').click
+
+ expect(page).to have_button('Expand all')
+
+ click_button 'Expand all'
+
+ expect(page).not_to have_button('Expand all')
+ end
+
context 'when in the inline view' do
include_examples 'unfold diffs'
end
diff --git a/spec/features/merge_request/user_views_open_merge_request_spec.rb b/spec/features/merge_request/user_views_open_merge_request_spec.rb
index 71022c6bb08..849fab62fc6 100644
--- a/spec/features/merge_request/user_views_open_merge_request_spec.rb
+++ b/spec/features/merge_request/user_views_open_merge_request_spec.rb
@@ -13,7 +13,7 @@ describe 'User views an open merge request' do
end
it 'renders both the title and the description' do
- node = find('.wiki h1 a#user-content-description-header')
+ node = find('.md h1 a#user-content-description-header')
expect(node[:href]).to end_with('#description-header')
# Work around a weird Capybara behavior where calling `parent` on a node
diff --git a/spec/features/merge_requests/user_filters_by_assignees_spec.rb b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
index d6c770c93f1..0cbf1bcae30 100644
--- a/spec/features/merge_requests/user_filters_by_assignees_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_assignees_spec.rb
@@ -7,7 +7,7 @@ describe 'Merge Requests > User filters by assignees', :js do
let(:user) { project.creator }
before do
- create(:merge_request, assignee: user, title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1')
+ create(:merge_request, assignees: [user], title: 'Bugfix1', source_project: project, target_project: project, source_branch: 'bugfix1')
create(:merge_request, title: 'Bugfix2', source_project: project, target_project: project, source_branch: 'bugfix2')
sign_in(user)
diff --git a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
index 1615899a047..4627931f26a 100644
--- a/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
+++ b/spec/features/merge_requests/user_filters_by_multiple_criteria_spec.rb
@@ -10,7 +10,7 @@ describe 'Merge requests > User filters by multiple criteria', :js do
before do
sign_in(user)
- mr = create(:merge_request, title: 'Bugfix2', author: user, assignee: user, source_project: project, target_project: project, milestone: milestone)
+ mr = create(:merge_request, title: 'Bugfix2', author: user, assignees: [user], source_project: project, target_project: project, milestone: milestone)
mr.labels << wontfix
visit project_merge_requests_path(project)
diff --git a/spec/features/merge_requests/user_filters_by_target_branch_spec.rb b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
new file mode 100644
index 00000000000..ffbdacc68f6
--- /dev/null
+++ b/spec/features/merge_requests/user_filters_by_target_branch_spec.rb
@@ -0,0 +1,45 @@
+require 'rails_helper'
+
+describe 'Merge Requests > User filters by target branch', :js do
+ include FilteredSearchHelpers
+
+ let!(:project) { create(:project, :public, :repository) }
+ let!(:user) { project.creator }
+ let!(:mr1) { create(:merge_request, source_project: project, target_project: project, source_branch: 'feature', target_branch: 'master') }
+ let!(:mr2) { create(:merge_request, source_project: project, target_project: project, source_branch: 'feature', target_branch: 'merged-target') }
+
+ before do
+ sign_in(user)
+ visit project_merge_requests_path(project)
+ end
+
+ context 'filtering by target-branch:master' do
+ it 'applies the filter' do
+ input_filtered_search('target-branch:master')
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).to have_content mr1.title
+ expect(page).not_to have_content mr2.title
+ end
+ end
+
+ context 'filtering by target-branch:merged-target' do
+ it 'applies the filter' do
+ input_filtered_search('target-branch:merged-target')
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).not_to have_content mr1.title
+ expect(page).to have_content mr2.title
+ end
+ end
+
+ context 'filtering by target-branch:feature' do
+ it 'applies the filter' do
+ input_filtered_search('target-branch:feature')
+
+ expect(page).to have_issuable_counts(open: 0, closed: 0, all: 0)
+ expect(page).not_to have_content mr1.title
+ expect(page).not_to have_content mr2.title
+ end
+ end
+end
diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
index ef7ae490b0f..bd91fae1453 100644
--- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb
+++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb
@@ -12,16 +12,16 @@ describe 'Merge requests > User lists merge requests' do
title: 'fix',
source_project: project,
source_branch: 'fix',
- assignee: user,
- milestone: create(:milestone, due_date: '2013-12-11'),
+ assignees: [user],
+ milestone: create(:milestone, project: project, due_date: '2013-12-11'),
created_at: 1.minute.ago,
updated_at: 1.minute.ago)
create(:merge_request,
title: 'markdown',
source_project: project,
source_branch: 'markdown',
- assignee: user,
- milestone: create(:milestone, due_date: '2013-12-12'),
+ assignees: [user],
+ milestone: create(:milestone, project: project, due_date: '2013-12-12'),
created_at: 2.minutes.ago,
updated_at: 2.minutes.ago)
create(:merge_request,
diff --git a/spec/features/merge_requests/user_mass_updates_spec.rb b/spec/features/merge_requests/user_mass_updates_spec.rb
index e535c7e5811..c2dd105324d 100644
--- a/spec/features/merge_requests/user_mass_updates_spec.rb
+++ b/spec/features/merge_requests/user_mass_updates_spec.rb
@@ -54,8 +54,7 @@ describe 'Merge requests > User mass updates', :js do
describe 'remove assignee' do
before do
- merge_request.assignee = user
- merge_request.save
+ merge_request.assignees = [user]
visit project_merge_requests_path(project)
end
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 6e349395017..adac59b89ef 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -122,4 +122,32 @@ describe 'Milestone' do
expect(page).to have_selector('.popover')
end
end
+
+ describe 'reopen closed milestones' do
+ before do
+ create(:milestone, :closed, project: project)
+ end
+
+ describe 'group milestones page' do
+ it 'reopens the milestone' do
+ visit group_milestones_path(group, { state: 'closed' })
+
+ click_link 'Reopen Milestone'
+
+ expect(page).not_to have_selector('.status-box-closed')
+ expect(page).to have_selector('.status-box-open')
+ end
+ end
+
+ describe 'project milestones page' do
+ it 'reopens the milestone' do
+ visit project_milestones_path(project, { state: 'closed' })
+
+ click_link 'Reopen Milestone'
+
+ expect(page).not_to have_selector('.status-box-closed')
+ expect(page).to have_selector('.status-box-open')
+ end
+ end
+ end
end
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index f4105730402..5ebfc32952d 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -14,7 +14,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
end
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
- :facebook, :cas3, :auth0, :authentiq]
+ :facebook, :cas3, :auth0, :authentiq, :salesforce]
before(:all) do
# The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost`
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/profiles/active_sessions_spec.rb
index d3050760c06..2aa0177af5d 100644
--- a/spec/features/profiles/active_sessions_spec.rb
+++ b/spec/features/profiles/active_sessions_spec.rb
@@ -7,6 +7,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
end
end
+ let(:admin) { create(:admin) }
+
around do |example|
Timecop.freeze(Time.zone.parse('2018-03-12 09:06')) do
example.run
@@ -16,6 +18,7 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
it 'User sees their active sessions' do
Capybara::Session.new(:session1)
Capybara::Session.new(:session2)
+ Capybara::Session.new(:session3)
# note: headers can only be set on the non-js (aka. rack-test) driver
using_session :session1 do
@@ -37,9 +40,27 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
gitlab_sign_in(user)
end
+ # set an admin session impersonating the user
+ using_session :session3 do
+ Capybara.page.driver.header(
+ 'User-Agent',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36'
+ )
+
+ gitlab_sign_in(admin)
+
+ visit admin_user_path(user)
+
+ click_link 'Impersonate'
+ end
+
using_session :session1 do
visit profile_active_sessions_path
+ expect(page).to(
+ have_selector('ul.list-group li.list-group-item', { text: 'Signed in on',
+ count: 2 }))
+
expect(page).to have_content(
'127.0.0.1 ' \
'This is your current session ' \
@@ -57,33 +78,8 @@ describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state do
)
expect(page).to have_selector '[title="Smartphone"]', count: 1
- end
- end
-
- it 'User can revoke a session', :js, :redis_session_store do
- Capybara::Session.new(:session1)
- Capybara::Session.new(:session2)
-
- # set an additional session in another browser
- using_session :session2 do
- gitlab_sign_in(user)
- end
-
- using_session :session1 do
- gitlab_sign_in(user)
- visit profile_active_sessions_path
-
- expect(page).to have_link('Revoke', count: 1)
-
- accept_confirm { click_on 'Revoke' }
-
- expect(page).not_to have_link('Revoke')
- end
-
- using_session :session2 do
- visit profile_active_sessions_path
- expect(page).to have_content('You need to sign in or sign up before continuing.')
+ expect(page).not_to have_content('Chrome on Windows')
end
end
end
diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb
new file mode 100644
index 00000000000..2d2da222998
--- /dev/null
+++ b/spec/features/profiles/user_edit_preferences_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'User edit preferences profile' do
+ let(:user) { create(:user) }
+
+ before do
+ stub_feature_flags(user_time_settings: true)
+ sign_in(user)
+ visit(profile_preferences_path)
+ end
+
+ it 'allows the user to toggle their time format preference' do
+ field = page.find_field("user[time_format_in_24h]")
+
+ expect(field).not_to be_checked
+
+ field.click
+
+ expect(field).to be_checked
+ end
+
+ it 'allows the user to toggle their time display preference' do
+ field = page.find_field("user[time_display_relative]")
+
+ expect(field).to be_checked
+
+ field.click
+
+ expect(field).not_to be_checked
+ end
+end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index b43711f6ef6..a53da94ef7d 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -327,5 +327,37 @@ describe 'User edit profile' do
end
end
end
+
+ context 'User time preferences', :js do
+ let(:issue) { create(:issue, project: project)}
+ let(:project) { create(:project) }
+
+ before do
+ stub_feature_flags(user_time_settings: true)
+ end
+
+ it 'shows the user time preferences form' do
+ expect(page).to have_content('Time settings')
+ end
+
+ it 'allows the user to select a time zone from a dropdown list of options' do
+ expect(page.find('.user-time-preferences .dropdown')).not_to have_css('.show')
+
+ page.find('.user-time-preferences .js-timezone-dropdown').click
+
+ expect(page.find('.user-time-preferences .dropdown')).to have_css('.show')
+
+ page.find("a", text: "Nuku'alofa").click
+
+ tz = page.find('.user-time-preferences #user_timezone', visible: false)
+
+ expect(tz.value).to eq('Pacific/Tongatapu')
+ end
+
+ it 'timezone defaults to servers default' do
+ timezone_name = Time.zone.tzinfo.name
+ expect(page.find('.user-time-preferences #user_timezone', visible: false).value).to eq(timezone_name)
+ end
+ end
end
end
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 9909bfb5904..1b3718968b9 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -63,7 +63,7 @@ describe 'User visits the profile preferences page' do
end
describe 'User changes their language', :js do
- it 'creates a flash message' do
+ it 'creates a flash message', :quarantine do
select2('en', from: '#user_preferred_language')
click_button 'Save'
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index a93df3696d2..95685a3c7ff 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Project variables', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') }
+ let(:variable) { create(:ci_variable, key: 'test_key', value: 'test_value', masked: true) }
let(:page_path) { project_settings_ci_cd_path(project) }
before do
diff --git a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
index 5f630c9ffa4..a1fcd4024c0 100644
--- a/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
+++ b/spec/features/projects/artifacts/user_browses_artifacts_spec.rb
@@ -19,6 +19,12 @@ describe "User browses artifacts" do
visit(browse_project_job_artifacts_path(project, job))
end
+ it "renders a link to the job in the breadcrumbs" do
+ page.within('.js-breadcrumbs-list') do
+ expect(page).to have_link("##{job.id}", href: project_job_path(project, job))
+ end
+ end
+
it "shows artifacts" do
expect(page).not_to have_selector(".build-sidebar")
diff --git a/spec/features/projects/badges/pipeline_badge_spec.rb b/spec/features/projects/badges/pipeline_badge_spec.rb
index dee81898928..4ac4e8f0fcb 100644
--- a/spec/features/projects/badges/pipeline_badge_spec.rb
+++ b/spec/features/projects/badges/pipeline_badge_spec.rb
@@ -41,6 +41,25 @@ describe 'Pipeline Badge' do
end
end
+ context 'when the pipeline is preparing' do
+ let!(:job) { create(:ci_build, status: 'created', pipeline: pipeline) }
+
+ before do
+ # Prevent skipping directly to 'pending'
+ allow(Ci::BuildPrepareWorker).to receive(:perform_async)
+ allow(job).to receive(:prerequisites).and_return([double])
+ end
+
+ it 'displays the preparing badge' do
+ job.enqueue
+
+ visit pipeline_project_badges_path(project, ref: ref, format: :svg)
+
+ expect(page.status_code).to eq(200)
+ expect_badge('preparing')
+ end
+ end
+
context 'when the pipeline is running' do
it 'shows displays so on the badge' do
create(:ci_build, pipeline: pipeline, name: 'second build', status_event: 'run')
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 3edcc7ac2cd..aa2e538cc8e 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -548,10 +548,7 @@ describe 'File blob', :js do
it 'displays an auxiliary viewer' do
aggregate_failures do
# shows names of dependency manager and package
- expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.')
-
- # shows a link to the gem
- expect(page).to have_link('activerecord', href: 'https://rubygems.org/gems/activerecord')
+ expect(page).to have_content('This project manages its dependencies using RubyGems.')
# shows a learn more link
expect(page).to have_link('Learn more', href: 'https://rubygems.org/')
@@ -575,7 +572,7 @@ describe 'File blob', :js do
visit_blob('files/ruby/test.rb', ref: 'feature')
end
- it 'should show the realtime pipeline status' do
+ it 'shows the realtime pipeline status' do
page.within('.commit-actions') do
expect(page).to have_css('.ci-status-icon')
expect(page).to have_css('.ci-status-icon-running')
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 1522a3361a1..57d21f3e182 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -9,6 +9,10 @@ describe 'Editing file blob', :js do
let(:file_path) { project.repository.ls_files(project.repository.root_ref)[1] }
let(:readme_file_path) { 'README.md' }
+ before do
+ stub_feature_flags(web_ide_default: false)
+ end
+
context 'as a developer' do
let(:user) { create(:user) }
let(:role) { :developer }
@@ -45,6 +49,15 @@ describe 'Editing file blob', :js do
end
end
+ it 'updates the content of file with a number as file path' do
+ project.repository.create_file(user, '1', 'test', message: 'testing', branch_name: branch)
+ visit project_blob_path(project, tree_join(branch, '1'))
+
+ edit_and_commit
+
+ expect(page).to have_content 'NextFeature'
+ end
+
context 'from blob file path' do
before do
visit project_blob_path(project, tree_join(branch, file_path))
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index c8dc72a34ec..3e75890725e 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -35,7 +35,7 @@ describe 'Download buttons in branches page' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, 'binary-encoding/download', job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/classification_label_on_project_pages_spec.rb b/spec/features/projects/classification_label_on_project_pages_spec.rb
new file mode 100644
index 00000000000..92f8aa8eb8d
--- /dev/null
+++ b/spec/features/projects/classification_label_on_project_pages_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Classification label on project pages' do
+ let(:project) do
+ create(:project, external_authorization_classification_label: 'authorized label')
+ end
+ let(:user) { create(:user) }
+
+ before do
+ stub_application_setting(external_authorization_service_enabled: true)
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'shows the classification label on the project page' do
+ visit project_path(project)
+
+ expect(page).to have_content('authorized label')
+ end
+end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 2c8d014c36d..527508b3519 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -17,7 +17,7 @@ describe 'Clusters Applications', :js do
end
context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project])}
+ let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
it 'user is unable to install applications' do
page.within('.js-cluster-application-row-helm') do
@@ -28,9 +28,11 @@ describe 'Clusters Applications', :js do
end
context 'when cluster is created' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project])}
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
it 'user can install applications' do
+ wait_for_requests
+
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
@@ -44,6 +46,8 @@ describe 'Clusters Applications', :js do
page.within('.js-cluster-application-row-helm') do
page.find(:css, '.js-cluster-application-install-button').click
end
+
+ wait_for_requests
end
it 'they see status transition' do
@@ -52,8 +56,6 @@ describe 'Clusters Applications', :js do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
- wait_until_helm_created!
-
Clusters::Cluster.last.application_helm.make_installing!
# FE starts polling and update the buttons to "Installing"
@@ -76,25 +78,79 @@ describe 'Clusters Applications', :js do
end
context 'on an abac cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project])}
+ let(:cluster) { create(:cluster, :provided_by_gcp, :rbac_disabled, projects: [project]) }
- it 'should show info block and not be installable' do
+ it 'shows info block and not be installable' do
page.within('.js-cluster-application-row-knative') do
- expect(page).to have_css('.bs-callout-info')
+ expect(page).to have_css('.rbac-notice')
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
end
end
end
context 'on an rbac cluster' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project])}
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- it 'should not show callout block and be installable' do
+ it 'does not show callout block and be installable' do
page.within('.js-cluster-application-row-knative') do
- expect(page).not_to have_css('.bs-callout-info')
+ expect(page).not_to have_css('.rbac-notice')
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
end
end
+
+ describe 'when user clicks install button' do
+ def domainname_form_value
+ page.find('.js-knative-domainname').value
+ end
+
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+
+ page.within('.js-cluster-application-row-knative') do
+ expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
+
+ page.find('.js-knative-domainname').set("domain.example.org")
+
+ click_button 'Install'
+
+ wait_for_requests
+
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+
+ Clusters::Cluster.last.application_knative.make_installing!
+ Clusters::Cluster.last.application_knative.make_installed!
+ Clusters::Cluster.last.application_knative.update_attribute(:external_ip, '127.0.0.1')
+ end
+ end
+
+ it 'shows status transition' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(domainname_form_value).to eq('domain.example.org')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
+ end
+
+ expect(page).to have_content('Knative was successfully installed on your Kubernetes cluster')
+ expect(page).to have_css('.js-knative-save-domain-button'), exact_text: 'Save changes'
+ end
+
+ it 'can then update the domain' do
+ page.within('.js-cluster-application-row-knative') do
+ expect(ClusterPatchAppWorker).to receive(:perform_async)
+
+ expect(domainname_form_value).to eq('domain.example.org')
+
+ page.find('.js-knative-domainname').set("new.domain.example.org")
+
+ click_button 'Save changes'
+
+ wait_for_requests
+
+ expect(domainname_form_value).to eq('new.domain.example.org')
+ end
+ end
+ end
end
end
@@ -148,6 +204,8 @@ describe 'Clusters Applications', :js do
page.within('.js-cluster-application-row-ingress') do
expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
page.find(:css, '.js-cluster-application-install-button').click
+
+ wait_for_requests
end
end
@@ -168,14 +226,14 @@ describe 'Clusters Applications', :js do
expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
expect(page).to have_css('.js-cluster-application-install-button[disabled]')
- expect(page).to have_selector('.js-no-ip-message')
- expect(page.find('.js-ip-address').value).to eq('?')
+ expect(page).to have_selector('.js-no-endpoint-message')
+ expect(page).to have_selector('.js-ingress-ip-loading-icon')
# We receive the external IP address and display
Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
- expect(page).not_to have_selector('.js-no-ip-message')
- expect(page.find('.js-ip-address').value).to eq('192.168.1.100')
+ expect(page).not_to have_selector('.js-no-endpoint-message')
+ expect(page.find('.js-endpoint').value).to eq('192.168.1.100')
end
expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
@@ -184,14 +242,4 @@ describe 'Clusters Applications', :js do
end
end
end
-
- def wait_until_helm_created!
- retries = 0
-
- while Clusters::Cluster.last.application_helm.nil?
- raise "Timed out waiting for helm application to be created in DB" if (retries += 1) > 3
-
- sleep(1)
- end
- end
end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 9322e29d744..83e582c34f0 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -92,7 +92,7 @@ describe 'Gcp Cluster', :js do
end
it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
+ expect(page).to have_css('.gl-field-error')
end
end
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index 1f2f7592d8b..31cc09ae911 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -12,6 +12,7 @@ describe 'User Cluster', :js do
allow(Projects::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+ allow_any_instance_of(Clusters::Cluster).to receive(:retrieve_connection_status).and_return(:connected)
end
context 'when user does not have a cluster and visits cluster index page' do
@@ -53,7 +54,7 @@ describe 'User Cluster', :js do
end
it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
+ expect(page).to have_css('.gl-field-error')
end
end
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index a85e7333ba8..ce382c19fc1 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Clusters', :js do
diff --git a/spec/features/projects/commit/comments/user_adds_comment_spec.rb b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
index 29442a58ea4..586e2e33112 100644
--- a/spec/features/projects/commit/comments/user_adds_comment_spec.rb
+++ b/spec/features/projects/commit/comments/user_adds_comment_spec.rb
@@ -138,7 +138,7 @@ describe "User adds a comment on a commit", :js do
click_button("Comment")
end
- page.within(".diff-file:nth-of-type(1) .notes_content.parallel.old") do
+ page.within(".diff-file:nth-of-type(1) .notes-content.parallel.old") do
expect(page).to have_content(old_comment)
end
@@ -152,7 +152,7 @@ describe "User adds a comment on a commit", :js do
wait_for_requests
- expect(all(".diff-file:nth-of-type(1) .notes_content.parallel.new")[1].text).to have_content(new_comment)
+ expect(all(".diff-file:nth-of-type(1) .notes-content.parallel.new")[1].text).to have_content(new_comment)
end
end
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index 19f6ebf2c1a..614f11c8392 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -43,7 +43,7 @@ describe 'Mini Pipeline Graph in Commit View', :js do
visit project_commit_path(project, project.commit.id)
end
- it 'should not display a mini pipeline graph' do
+ it 'does not display a mini pipeline graph' do
expect(page).not_to have_selector('.mr-widget-pipeline-graph')
end
end
diff --git a/spec/features/projects/commits/user_browses_commits_spec.rb b/spec/features/projects/commits/user_browses_commits_spec.rb
index 574a8aefd63..953517cdff9 100644
--- a/spec/features/projects/commits/user_browses_commits_spec.rb
+++ b/spec/features/projects/commits/user_browses_commits_spec.rb
@@ -61,7 +61,7 @@ describe 'User browses commits' do
it 'renders commit ci info' do
visit project_commit_path(project, sample_commit.id)
- expect(page).to have_content "Pipeline ##{pipeline.id} pending"
+ expect(page).to have_content "Pipeline ##{pipeline.id} (##{pipeline.iid}) pending"
end
end
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index a8a3b6910fb..da4ef6428d4 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -120,7 +120,7 @@ describe 'Environment' do
end
it 'does show a play button' do
- expect(page).to have_link(action.name.humanize)
+ expect(page).to have_link(action.name)
end
it 'does allow to play manual action', :js do
@@ -128,7 +128,7 @@ describe 'Environment' do
find('button.dropdown').click
- expect { click_link(action.name.humanize) }
+ expect { click_link(action.name) }
.not_to change { Ci::Pipeline.count }
wait_for_all_requests
@@ -140,7 +140,7 @@ describe 'Environment' do
context 'when user has no ability to trigger a deployment' do
it 'does not show a play button' do
- expect(page).not_to have_link(action.name.humanize)
+ expect(page).not_to have_link(action.name)
end
end
@@ -159,7 +159,7 @@ describe 'Environment' do
context 'for project maintainer' do
let(:role) { :maintainer }
- it 'it shows the terminal button' do
+ it 'shows the terminal button' do
expect(page).to have_terminal_button
end
@@ -319,7 +319,7 @@ describe 'Environment' do
yield
- GitPushService.new(project, user, params).execute
+ Git::BranchPushService.new(project, user, params).execute
end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 0c517d5f490..7b7e45312d9 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -30,7 +30,7 @@ describe 'Environments page', :js do
end
describe 'in available tab page' do
- it 'should show one environment' do
+ it 'shows one environment' do
visit_environments(project, scope: 'available')
expect(page).to have_css('.environments-container')
@@ -38,8 +38,25 @@ describe 'Environments page', :js do
end
end
+ describe 'with environments spanning multiple pages', :js do
+ before do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(3)
+ create_list(:environment, 4, project: project, state: :available)
+ end
+
+ it 'renders second page of pipelines' do
+ visit_environments(project, scope: 'available')
+
+ find('.js-next-button').click
+ wait_for_requests
+
+ expect(page).to have_selector('.gl-pagination .page', count: 2)
+ expect(find('.gl-pagination .page-item.active .page-link').text).to eq("2")
+ end
+ end
+
describe 'in stopped tab page' do
- it 'should show no environments' do
+ it 'shows no environments' do
visit_environments(project, scope: 'stopped')
expect(page).to have_css('.environments-container')
@@ -55,7 +72,7 @@ describe 'Environments page', :js do
allow_any_instance_of(Kubeclient::Client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
end
- it 'should show one environment without error' do
+ it 'shows one environment without error' do
visit_environments(project, scope: 'available')
expect(page).to have_css('.environments-container')
@@ -70,7 +87,7 @@ describe 'Environments page', :js do
end
describe 'in available tab page' do
- it 'should show no environments' do
+ it 'shows no environments' do
visit_environments(project, scope: 'available')
expect(page).to have_css('.environments-container')
@@ -79,7 +96,7 @@ describe 'Environments page', :js do
end
describe 'in stopped tab page' do
- it 'should show one environment' do
+ it 'shows one environment' do
visit_environments(project, scope: 'stopped')
expect(page).to have_css('.environments-container')
@@ -166,14 +183,14 @@ describe 'Environments page', :js do
it 'shows a play button' do
find('.js-environment-actions-dropdown').click
- expect(page).to have_content(action.name.humanize)
+ expect(page).to have_content(action.name)
end
it 'allows to play a manual action', :js do
expect(action).to be_manual
find('.js-environment-actions-dropdown').click
- expect(page).to have_content(action.name.humanize)
+ expect(page).to have_content(action.name)
expect { find('.js-manual-action-link').click }
.not_to change { Ci::Pipeline.count }
@@ -294,7 +311,7 @@ describe 'Environments page', :js do
it "has link to the delayed job's action" do
find('.js-environment-actions-dropdown').click
- expect(page).to have_button('Delayed job')
+ expect(page).to have_button('delayed job')
expect(page).to have_content(/\d{2}:\d{2}:\d{2}/)
end
@@ -316,7 +333,7 @@ describe 'Environments page', :js do
context 'when user played a delayed job immediately' do
before do
find('.js-environment-actions-dropdown').click
- page.accept_confirm { click_button('Delayed job') }
+ page.accept_confirm { click_button('delayed job') }
wait_for_requests
end
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
index 03cb3530e2b..111972a6b00 100644
--- a/spec/features/projects/files/download_buttons_spec.rb
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -30,7 +30,7 @@ describe 'Projects > Files > Download buttons in files tree' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
index b6dbf76bc9b..51c884201a6 100644
--- a/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
+++ b/spec/features/projects/files/files_sort_submodules_with_folders_spec.rb
@@ -5,6 +5,8 @@ describe 'Projects > Files > User views files page' do
let(:user) { project.owner }
before do
+ stub_feature_flags(vue_file_list: false)
+
sign_in user
visit project_tree_path(project, project.repository.root_ref)
end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
index 6762460971f..44715261b8b 100644
--- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -5,6 +5,7 @@ describe 'Projects > Files > Project owner creates a license file', :js do
let(:project_maintainer) { project.owner }
before do
+ stub_feature_flags(vue_file_list: false)
project.repository.delete_file(project_maintainer, 'LICENSE',
message: 'Remove LICENSE', branch_name: 'master')
sign_in(project_maintainer)
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index 66268355345..a5d849db8a3 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -11,6 +11,7 @@ describe "User browses files" do
let(:user) { project.owner }
before do
+ stub_feature_flags(vue_file_list: false)
stub_feature_flags(csslab: false)
sign_in(user)
end
diff --git a/spec/features/projects/files/user_browses_lfs_files_spec.rb b/spec/features/projects/files/user_browses_lfs_files_spec.rb
index d56476adb05..d5cb8f9212d 100644
--- a/spec/features/projects/files/user_browses_lfs_files_spec.rb
+++ b/spec/features/projects/files/user_browses_lfs_files_spec.rb
@@ -5,6 +5,8 @@ describe 'Projects > Files > User browses LFS files' do
let(:user) { project.owner }
before do
+ stub_feature_flags(vue_file_list: false)
+
sign_in(user)
end
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb
index 847b5f0860f..e29e867492e 100644
--- a/spec/features/projects/files/user_creates_directory_spec.rb
+++ b/spec/features/projects/files/user_creates_directory_spec.rb
@@ -11,6 +11,8 @@ describe 'Projects > Files > User creates a directory', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
+
project.add_developer(user)
sign_in(user)
visit project_tree_path(project, 'master')
diff --git a/spec/features/projects/files/user_creates_files_spec.rb b/spec/features/projects/files/user_creates_files_spec.rb
index a4f94b7a76d..69f8bd4d319 100644
--- a/spec/features/projects/files/user_creates_files_spec.rb
+++ b/spec/features/projects/files/user_creates_files_spec.rb
@@ -12,6 +12,9 @@ describe 'Projects > Files > User creates files' do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
+ stub_feature_flags(web_ide_default: false)
+
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/files/user_deletes_files_spec.rb b/spec/features/projects/files/user_deletes_files_spec.rb
index 614b11fa5c8..11ee87f245b 100644
--- a/spec/features/projects/files/user_deletes_files_spec.rb
+++ b/spec/features/projects/files/user_deletes_files_spec.rb
@@ -12,6 +12,8 @@ describe 'Projects > Files > User deletes files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
+
sign_in(user)
end
diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb
index 9eb65ec159c..26efb5e6787 100644
--- a/spec/features/projects/files/user_edits_files_spec.rb
+++ b/spec/features/projects/files/user_edits_files_spec.rb
@@ -9,6 +9,9 @@ describe 'Projects > Files > User edits files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(web_ide_default: false)
+ stub_feature_flags(vue_file_list: false)
+
sign_in(user)
end
@@ -169,7 +172,7 @@ describe 'Projects > Files > User edits files', :js do
wait_for_requests
end
- it 'links to the forked project for editing' do
+ it 'links to the forked project for editing', :quarantine do
click_link('.gitignore')
find('.js-edit-blob').click
diff --git a/spec/features/projects/files/user_replaces_files_spec.rb b/spec/features/projects/files/user_replaces_files_spec.rb
index e3da28d73c3..bfd612e4cc8 100644
--- a/spec/features/projects/files/user_replaces_files_spec.rb
+++ b/spec/features/projects/files/user_replaces_files_spec.rb
@@ -14,6 +14,8 @@ describe 'Projects > Files > User replaces files', :js do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
+
sign_in(user)
end
diff --git a/spec/features/projects/files/user_uploads_files_spec.rb b/spec/features/projects/files/user_uploads_files_spec.rb
index af3fc528a20..25ff3fdf411 100644
--- a/spec/features/projects/files/user_uploads_files_spec.rb
+++ b/spec/features/projects/files/user_uploads_files_spec.rb
@@ -14,6 +14,8 @@ describe 'Projects > Files > User uploads files' do
let(:project2_tree_path_root_ref) { project_tree_path(project2, project2.repository.root_ref) }
before do
+ stub_feature_flags(vue_file_list: false)
+
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/forks/fork_list_spec.rb b/spec/features/projects/forks/fork_list_spec.rb
new file mode 100644
index 00000000000..2c41c61a660
--- /dev/null
+++ b/spec/features/projects/forks/fork_list_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe 'listing forks of a project' do
+ include ProjectForksHelper
+ include ExternalAuthorizationServiceHelpers
+
+ let(:source) { create(:project, :public, :repository) }
+ let!(:fork) { fork_project(source, nil, repository: true) }
+ let(:user) { create(:user) }
+
+ before do
+ source.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'shows the forked project in the list with commit as description' do
+ visit project_forks_path(source)
+
+ page.within('li.project-row') do
+ expect(page).to have_content(fork.full_name)
+ expect(page).to have_css('a.commit-row-message')
+ end
+ end
+
+ it 'does not show the commit message when an external authorization service is used' do
+ enable_external_authorization_service_check
+
+ visit project_forks_path(source)
+
+ page.within('li.project-row') do
+ expect(page).to have_content(fork.full_name)
+ expect(page).not_to have_css('a.commit-row-message')
+ end
+ end
+end
diff --git a/spec/features/projects/graph_spec.rb b/spec/features/projects/graph_spec.rb
index 9665f1755d6..e1bc18519a2 100644
--- a/spec/features/projects/graph_spec.rb
+++ b/spec/features/projects/graph_spec.rb
@@ -6,6 +6,8 @@ describe 'Project Graph', :js do
let(:branch_name) { 'master' }
before do
+ ::Projects::DetectRepositoryLanguagesService.new(project, user).execute
+
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index f76f9ba7577..9d74a96ab3d 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -12,7 +12,7 @@ describe 'Import/Export - project export integration test', :js do
let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
- let(:sensitive_words) { %w[pass secret token key encrypted] }
+ let(:sensitive_words) { %w[pass secret token key encrypted html] }
let(:safe_list) do
{
token: [ProjectHook, Ci::Trigger, CommitStatus],
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 28ae90bc0de..8d2b1fc7e30 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -47,7 +47,6 @@ describe 'Import/Export - project import integration test', :js do
expect(project.description).to eq("Foo Bar")
expect(project.issues).not_to be_empty
expect(project.merge_requests).not_to be_empty
- expect(project_hook_exists?(project)).to be true
expect(wiki_exists?(project)).to be true
expect(project.import_state.status).to eq('finished')
end
diff --git a/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
new file mode 100644
index 00000000000..a8612d77a5e
--- /dev/null
+++ b/spec/features/projects/issues/viewing_issues_with_external_authorization_enabled_spec.rb
@@ -0,0 +1,128 @@
+require 'spec_helper'
+
+describe 'viewing an issue with cross project references' do
+ include ExternalAuthorizationServiceHelpers
+ include Gitlab::Routing.url_helpers
+
+ let(:user) { create(:user) }
+ let(:other_project) do
+ create(:project, :public,
+ external_authorization_classification_label: 'other_label')
+ end
+ let(:other_issue) do
+ create(:issue, :closed,
+ title: 'I am in another project',
+ project: other_project)
+ end
+ let(:other_confidential_issue) do
+ create(:issue, :confidential, :closed,
+ title: 'I am in another project and confidential',
+ project: other_project)
+ end
+ let(:other_merge_request) do
+ create(:merge_request, :closed,
+ title: 'I am a merge request in another project',
+ source_project: other_project)
+ end
+ let(:description_referencing_other_issue) do
+ "Referencing: #{other_issue.to_reference(project)}, "\
+ "a confidential issue #{confidential_issue.to_reference}, "\
+ "a cross project confidential issue #{other_confidential_issue.to_reference(project)}, and "\
+ "a cross project merge request #{other_merge_request.to_reference(project)}"
+ end
+ let(:project) { create(:project) }
+ let(:issue) do
+ create(:issue,
+ project: project,
+ description: description_referencing_other_issue )
+ end
+ let(:confidential_issue) do
+ create(:issue, :confidential, :closed,
+ title: "I am in the same project and confidential",
+ project: project)
+ end
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ end
+
+ it 'shows all information related to the cross project reference' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{other_issue.to_reference(project)} (#{other_issue.state})")
+ expect(page).to have_xpath("//a[@title='#{other_issue.title}']")
+ end
+
+ it 'shows a link to the confidential issue in the same project' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})")
+ expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']")
+ end
+
+ it 'does not show the link to a cross project confidential issue when the user does not have access' do
+ visit project_issue_path(project, issue)
+
+ expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})")
+ expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
+ end
+
+ it 'shows the link to a cross project confidential issue when the user has access' do
+ other_project.add_developer(user)
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{other_confidential_issue.to_reference(project)} (#{other_confidential_issue.state})")
+ expect(page).to have_xpath("//a[@title='#{other_confidential_issue.title}']")
+ end
+
+ context 'when an external authorization service is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'only hits the external service for the project the user is viewing' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'default_label', any_args).at_least(1).and_return(true)
+ expect(::Gitlab::ExternalAuthorization)
+ .not_to receive(:access_allowed?).with(user, 'other_label', any_args)
+
+ visit project_issue_path(project, issue)
+ end
+
+ it 'shows only the link to the cross project references' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{other_issue.to_reference(project)}")
+ expect(page).to have_link("#{other_merge_request.to_reference(project)}")
+ expect(page).not_to have_content("#{other_issue.to_reference(project)} (#{other_issue.state})")
+ expect(page).not_to have_xpath("//a[@title='#{other_issue.title}']")
+ expect(page).not_to have_content("#{other_merge_request.to_reference(project)} (#{other_merge_request.state})")
+ expect(page).not_to have_xpath("//a[@title='#{other_merge_request.title}']")
+ end
+
+ it 'does not link a cross project confidential issue if the user does not have access' do
+ visit project_issue_path(project, issue)
+
+ expect(page).not_to have_link("#{other_confidential_issue.to_reference(project)}")
+ expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
+ end
+
+ it 'links a cross project confidential issue without exposing information when the user has access' do
+ other_project.add_developer(user)
+
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{other_confidential_issue.to_reference(project)}")
+ expect(page).not_to have_xpath("//a[@title='#{other_confidential_issue.title}']")
+ end
+
+ it 'shows a link to the confidential issue in the same project' do
+ visit project_issue_path(project, issue)
+
+ expect(page).to have_link("#{confidential_issue.to_reference(project)} (#{confidential_issue.state})")
+ expect(page).to have_xpath("//a[@title='#{confidential_issue.title}']")
+ end
+ end
+end
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index 6ce37297a7e..b5e711997a0 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -90,7 +90,7 @@ describe 'Project Jobs Permissions' do
before do
archive = fixture_file_upload('spec/fixtures/ci_build_artifacts.zip')
- job.update(legacy_artifacts_file: archive)
+ create(:ci_job_artifact, :archive, file: archive, job: job)
end
context 'when public access for jobs is disabled' do
diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb
index 908c616f2fc..54b462da87a 100644
--- a/spec/features/projects/jobs/user_browses_job_spec.rb
+++ b/spec/features/projects/jobs/user_browses_job_spec.rb
@@ -28,8 +28,8 @@ describe 'User browses a job', :js do
expect(page).to have_no_css('.artifacts')
expect(build).not_to have_trace
- expect(build.artifacts_file.exists?).to be_falsy
- expect(build.artifacts_metadata.exists?).to be_falsy
+ expect(build.artifacts_file.present?).to be_falsy
+ expect(build.artifacts_metadata.present?).to be_falsy
expect(page).to have_content('Job has been erased')
end
diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb
index ebc20d15d67..bd6c73f4b85 100644
--- a/spec/features/projects/jobs/user_browses_jobs_spec.rb
+++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb
@@ -16,6 +16,12 @@ describe 'User browses jobs' do
visit(project_jobs_path(project))
end
+ it 'shows pipeline id and IID' do
+ page.within('td.pipeline-link') do
+ expect(page).to have_content("##{pipeline.id} (##{pipeline.iid})")
+ end
+ end
+
it 'shows the coverage' do
page.within('td.coverage') do
expect(page).to have_content('99.9%')
diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb
index 65ce872363f..03562bd382e 100644
--- a/spec/features/projects/jobs_spec.rb
+++ b/spec/features/projects/jobs_spec.rb
@@ -2,6 +2,9 @@ require 'spec_helper'
require 'tempfile'
describe 'Jobs', :clean_gitlab_redis_shared_state do
+ include Gitlab::Routing
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:user_access_level) { :developer }
let(:project) { create(:project, :repository) }
@@ -121,6 +124,112 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
end
end
+ context 'pipeline info block', :js do
+ it 'shows pipeline id and source branch' do
+ visit project_job_path(project, job)
+
+ within '.js-pipeline-info' do
+ expect(page).to have_content("Pipeline ##{pipeline.id} (##{pipeline.iid}) for #{pipeline.ref}")
+ end
+ end
+
+ context 'when pipeline is detached merge request pipeline' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_detached_merge_request_pipeline,
+ target_project: target_project,
+ source_project: source_project)
+ end
+
+ let(:source_project) { project }
+ let(:target_project) { project }
+ let(:pipeline) { merge_request.all_pipelines.last }
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'shows merge request iid and source branch' do
+ visit project_job_path(project, job)
+
+ within '.js-pipeline-info' do
+ expect(page).to have_content("for !#{pipeline.merge_request.iid} " \
+ "with #{pipeline.merge_request.source_branch}")
+ expect(page).to have_link("!#{pipeline.merge_request.iid}",
+ href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(pipeline.merge_request.source_branch,
+ href: project_commits_path(project, merge_request.source_branch))
+ end
+ end
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+ let(:target_project) { project }
+
+ it 'shows merge request iid and source branch' do
+ visit project_job_path(source_project, job)
+
+ within '.js-pipeline-info' do
+ expect(page).to have_content("for !#{pipeline.merge_request.iid} " \
+ "with #{pipeline.merge_request.source_branch}")
+ expect(page).to have_link("!#{pipeline.merge_request.iid}",
+ href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(pipeline.merge_request.source_branch,
+ href: project_commits_path(source_project, merge_request.source_branch))
+ end
+ end
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ target_project: target_project,
+ source_project: source_project)
+ end
+
+ let(:source_project) { project }
+ let(:target_project) { project }
+ let(:pipeline) { merge_request.all_pipelines.last }
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'shows merge request iid and source branch' do
+ visit project_job_path(project, job)
+
+ within '.js-pipeline-info' do
+ expect(page).to have_content("for !#{pipeline.merge_request.iid} " \
+ "with #{pipeline.merge_request.source_branch} " \
+ "into #{pipeline.merge_request.target_branch}")
+ expect(page).to have_link("!#{pipeline.merge_request.iid}",
+ href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(pipeline.merge_request.source_branch,
+ href: project_commits_path(project, merge_request.source_branch))
+ expect(page).to have_link(pipeline.merge_request.target_branch,
+ href: project_commits_path(project, merge_request.target_branch))
+ end
+ end
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+ let(:target_project) { project }
+
+ it 'shows merge request iid and source branch' do
+ visit project_job_path(source_project, job)
+
+ within '.js-pipeline-info' do
+ expect(page).to have_content("for !#{pipeline.merge_request.iid} " \
+ "with #{pipeline.merge_request.source_branch} " \
+ "into #{pipeline.merge_request.target_branch}")
+ expect(page).to have_link("!#{pipeline.merge_request.iid}",
+ href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(pipeline.merge_request.source_branch,
+ href: project_commits_path(source_project, merge_request.source_branch))
+ expect(page).to have_link(pipeline.merge_request.target_branch,
+ href: project_commits_path(project, merge_request.target_branch))
+ end
+ end
+ end
+ end
+ end
+
context 'sidebar', :js do
let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline, name: '<img src=x onerror=alert(document.domain)>') }
@@ -205,7 +314,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context "Download artifacts", :js do
before do
- job.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: job)
visit project_job_path(project, job)
end
@@ -229,8 +338,8 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context 'Artifacts expire date', :js do
before do
- job.update(legacy_artifacts_file: artifacts_file,
- artifacts_expire_at: expire_at)
+ create(:ci_job_artifact, :archive, file: artifacts_file, expire_at: expire_at, job: job)
+ job.update!(artifacts_expire_at: expire_at)
visit project_job_path(project, job)
end
@@ -872,7 +981,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
describe "GET /:project/jobs/:id/download", :js do
before do
- job.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: job)
visit project_job_path(project, job)
click_link 'Download'
@@ -880,7 +989,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do
context "Build from other project" do
before do
- job2.update(legacy_artifacts_file: artifacts_file)
+ create(:ci_job_artifact, :archive, file: artifacts_file, job: job2)
end
it do
diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb
index d36f043f880..f32b155790f 100644
--- a/spec/features/projects/labels/update_prioritization_spec.rb
+++ b/spec/features/projects/labels/update_prioritization_spec.rb
@@ -138,29 +138,41 @@ describe 'Prioritize labels' do
end
context 'as a guest' do
- it 'does not prioritize labels' do
+ before do
+ create(:label_priority, project: project, label: bug, priority: 1)
+ create(:label_priority, project: project, label: feature, priority: 2)
+
guest = create(:user)
sign_in guest
visit project_labels_path(project)
+ end
+ it 'cannot prioritize labels' do
expect(page).to have_content 'bug'
expect(page).to have_content 'wontfix'
expect(page).to have_content 'feature'
- expect(page).not_to have_css('.prioritized-labels')
expect(page).not_to have_content 'Star a label'
end
+
+ it 'cannot sort prioritized labels', :js do
+ drag_to(selector: '.prioritized-labels .label-list-item', from_index: 1, to_index: 2)
+
+ page.within('.prioritized-labels') do
+ expect(first('.label-list-item')).to have_content('bug')
+ expect(page.all('.label-list-item').last).to have_content('feature')
+ end
+ end
end
context 'as a non signed in user' do
- it 'does not prioritize labels' do
+ it 'cannot prioritize labels' do
visit project_labels_path(project)
expect(page).to have_content 'bug'
expect(page).to have_content 'wontfix'
expect(page).to have_content 'feature'
- expect(page).not_to have_css('.prioritized-labels')
expect(page).not_to have_content 'Star a label'
end
end
diff --git a/spec/features/projects/labels/user_promotes_label_spec.rb b/spec/features/projects/labels/user_promotes_label_spec.rb
new file mode 100644
index 00000000000..fdecafd4c50
--- /dev/null
+++ b/spec/features/projects/labels/user_promotes_label_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User promotes label' do
+ set(:group) { create(:group) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project, namespace: group) }
+ set(:label) { create(:label, project: project) }
+
+ context 'when user can admin group labels' do
+ before do
+ group.add_developer(user)
+ sign_in(user)
+ visit(project_labels_path(project))
+ end
+
+ it "shows label promote button" do
+ expect(page).to have_selector('.js-promote-project-label-button')
+ end
+ end
+
+ context 'when user cannot admin group labels' do
+ before do
+ project.add_developer(user)
+ sign_in(user)
+ visit(project_labels_path(project))
+ end
+
+ it "does not show label promote button" do
+ expect(page).not_to have_selector('.js-promote-project-label-button')
+ end
+ end
+end
diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb
index b0ce03a1c31..c231e54decd 100644
--- a/spec/features/projects/labels/user_removes_labels_spec.rb
+++ b/spec/features/projects/labels/user_removes_labels_spec.rb
@@ -21,8 +21,11 @@ describe "User removes labels" do
page.first(".label-list-item") do
first('.js-label-options-dropdown').click
first(".remove-row").click
- first(:link, "Delete label").click
end
+
+ expect(page).to have_content("#{label.title} will be permanently deleted from #{project.name}. This cannot be undone.")
+
+ first(:link, "Delete label").click
end
expect(page).to have_content("Label was removed").and have_no_content(label.title)
diff --git a/spec/features/projects/labels/user_views_labels_spec.rb b/spec/features/projects/labels/user_views_labels_spec.rb
index 2c8267764bd..a6f7968c535 100644
--- a/spec/features/projects/labels/user_views_labels_spec.rb
+++ b/spec/features/projects/labels/user_views_labels_spec.rb
@@ -7,6 +7,7 @@ describe "User views labels" do
set(:user) { create(:user) }
let(:label_titles) { %w[bug enhancement feature] }
+ let!(:prioritized_label) { create(:label, project: project, title: 'prioritized-label-name', priority: 1) }
before do
label_titles.each { |title| create(:label, project: project, title: title) }
@@ -18,6 +19,10 @@ describe "User views labels" do
end
it "shows all labels" do
+ page.within('.prioritized-labels .manage-labels-list') do
+ expect(page).to have_content('prioritized-label-name')
+ end
+
page.within('.other-labels .manage-labels-list') do
label_titles.each { |title| expect(page).to have_content(title) }
end
diff --git a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
index 0ab29660189..a645b917568 100644
--- a/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
+++ b/spec/features/projects/members/group_member_cannot_leave_group_project_spec.rb
@@ -8,10 +8,17 @@ describe 'Projects > Members > Group member cannot leave group project' do
before do
group.add_developer(user)
sign_in(user)
- visit project_path(project)
end
it 'user does not see a "Leave project" link' do
+ visit project_path(project)
+
expect(page).not_to have_content 'Leave project'
end
+
+ it 'renders a flash message if attempting to leave by url', :js do
+ visit project_path(project, leave: 1)
+
+ expect(find('.flash-alert')).to have_content 'You do not have permission to leave this project'
+ end
end
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
index fceead0b45e..7432c600c1e 100644
--- a/spec/features/projects/members/invite_group_spec.rb
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -27,6 +27,7 @@ describe 'Project > Members > Invite group', :js do
before do
project.add_maintainer(maintainer)
+ group_to_share_with.add_guest(maintainer)
sign_in(maintainer)
end
@@ -112,6 +113,7 @@ describe 'Project > Members > Invite group', :js do
before do
project.add_maintainer(maintainer)
+ group.add_guest(maintainer)
sign_in(maintainer)
visit project_settings_members_path(project)
@@ -157,7 +159,7 @@ describe 'Project > Members > Invite group', :js do
open_select2 '#link_group_id'
end
- it 'should infinitely scroll' do
+ it 'infinitely scrolls' do
expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
scroll_select2_to_bottom('.select2-drop .select2-results:visible')
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
index 94b29de4686..bd2ef9c07c4 100644
--- a/spec/features/projects/members/member_leaves_project_spec.rb
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -7,13 +7,24 @@ describe 'Projects > Members > Member leaves project' do
before do
project.add_developer(user)
sign_in(user)
- visit project_path(project)
end
it 'user leaves project' do
+ visit project_path(project)
+
click_link 'Leave project'
expect(current_path).to eq(dashboard_projects_path)
expect(project.users.exists?(user.id)).to be_falsey
end
+
+ it 'user leaves project by url param', :js do
+ visit project_path(project, leave: 1)
+
+ page.accept_confirm
+
+ expect(find('.flash-notice')).to have_content "You left the \"#{project.full_name}\" project"
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(project.users.exists?(user.id)).to be_falsey
+ end
end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 50ba67f0ffc..f26941ab567 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -62,8 +62,9 @@ describe 'Projects > Members > User requests access', :js do
accept_confirm { click_link 'Withdraw Access Request' }
- expect(project.requesters.exists?(user_id: user)).to be_falsey
expect(page).to have_content 'Your access request to the project has been withdrawn.'
+ expect(page).not_to have_content 'Withdraw Access Request'
+ expect(page).to have_content 'Request Access'
end
def open_project_settings_menu
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index 75c72a68069..033e1afe866 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -63,6 +63,36 @@ describe 'New project' do
end
end
end
+
+ context 'when group visibility is private but default is internal' do
+ before do
+ stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'has private selected' do
+ group = create(:group, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ visit new_project_path(namespace_id: group.id)
+
+ page.within('#blank-project-pane') do
+ expect(find_field("project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).to be_checked
+ end
+ end
+ end
+
+ context 'when group visibility is public but user requests private' do
+ before do
+ stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
+ end
+
+ it 'has private selected' do
+ group = create(:group, visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ visit new_project_path(namespace_id: group.id, project: { visibility_level: Gitlab::VisibilityLevel::PRIVATE })
+
+ page.within('#blank-project-pane') do
+ expect(find_field("project_visibility_level_#{Gitlab::VisibilityLevel::PRIVATE}")).to be_checked
+ end
+ end
+ end
end
context 'Readme selector' do
@@ -252,4 +282,23 @@ describe 'New project' do
end
end
end
+
+ context 'Namespace selector' do
+ context 'with group with DEVELOPER_MAINTAINER_PROJECT_ACCESS project_creation_level' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+
+ before do
+ group.add_developer(user)
+ visit new_project_path(namespace_id: group.id)
+ end
+
+ it 'selects the group namespace' do
+ page.within('#blank-project-pane') do
+ namespace = find('#project_namespace_id option[selected]')
+
+ expect(namespace.text).to eq group.full_path
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/pages_lets_encrypt_spec.rb b/spec/features/projects/pages_lets_encrypt_spec.rb
new file mode 100644
index 00000000000..baa217cbe58
--- /dev/null
+++ b/spec/features/projects/pages_lets_encrypt_spec.rb
@@ -0,0 +1,131 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe "Pages with Let's Encrypt", :https_pages_enabled do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:role) { :maintainer }
+ let(:certificate_pem) do
+ <<~PEM
+ -----BEGIN CERTIFICATE-----
+ MIICGzCCAYSgAwIBAgIBATANBgkqhkiG9w0BAQUFADAbMRkwFwYDVQQDExB0ZXN0
+ LWNlcnRpZmljYXRlMB4XDTE2MDIxMjE0MzIwMFoXDTIwMDQxMjE0MzIwMFowGzEZ
+ MBcGA1UEAxMQdGVzdC1jZXJ0aWZpY2F0ZTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
+ gYkCgYEApL4J9L0ZxFJ1hI1LPIflAlAGvm6ZEvoT4qKU5Xf2JgU7/2geNR1qlNFa
+ SvCc08Knupp5yTgmvyK/Xi09U0N82vvp4Zvr/diSc4A/RA6Mta6egLySNT438kdT
+ nY2tR5feoTLwQpX0t4IMlwGQGT5h6Of2fKmDxzuwuyffcIHqLdsCAwEAAaNvMG0w
+ DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxl9WSxBprB0z0ibJs3rXEk0+95AwCwYD
+ VR0PBAQDAgXgMBEGCWCGSAGG+EIBAQQEAwIGQDAeBglghkgBhvhCAQ0EERYPeGNh
+ IGNlcnRpZmljYXRlMA0GCSqGSIb3DQEBBQUAA4GBAGC4T8SlFHK0yPSa+idGLQFQ
+ joZp2JHYvNlTPkRJ/J4TcXxBTJmArcQgTIuNoBtC+0A/SwdK4MfTCUY4vNWNdese
+ 5A4K65Nb7Oh1AdQieTBHNXXCdyFsva9/ScfQGEl7p55a52jOPs0StPd7g64uvjlg
+ YHi2yesCrOvVXt+lgPTd
+ -----END CERTIFICATE-----
+ PEM
+ end
+
+ let(:certificate_key) do
+ <<~KEY
+ -----BEGIN PRIVATE KEY-----
+ MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKS+CfS9GcRSdYSN
+ SzyH5QJQBr5umRL6E+KilOV39iYFO/9oHjUdapTRWkrwnNPCp7qaeck4Jr8iv14t
+ PVNDfNr76eGb6/3YknOAP0QOjLWunoC8kjU+N/JHU52NrUeX3qEy8EKV9LeCDJcB
+ kBk+Yejn9nypg8c7sLsn33CB6i3bAgMBAAECgYA2D26w80T7WZvazYr86BNMePpd
+ j2mIAqx32KZHzt/lhh40J/SRtX9+Kl0Y7nBoRR5Ja9u/HkAIxNxLiUjwg9r6cpg/
+ uITEF5nMt7lAk391BuI+7VOZZGbJDsq2ulPd6lO+C8Kq/PI/e4kXcIjeH6KwQsuR
+ 5vrXfBZ3sQfflaiN4QJBANBt8JY2LIGQF8o89qwUpRL5vbnKQ4IzZ5+TOl4RLR7O
+ AQpJ81tGuINghO7aunctb6rrcKJrxmEH1whzComybrMCQQDKV49nOBudRBAIgG4K
+ EnLzsRKISUHMZSJiYTYnablof8cKw1JaQduw7zgrUlLwnroSaAGX88+Jw1f5n2Lh
+ Vlg5AkBDdUGnrDLtYBCDEQYZHblrkc7ZAeCllDOWjxUV+uMqlCv8A4Ey6omvY57C
+ m6I8DkWVAQx8VPtozhvHjUw80rZHAkB55HWHAM3h13axKG0htCt7klhPsZHpx6MH
+ EPjGlXIT+aW2XiPmK3ZlCDcWIenE+lmtbOpI159Wpk8BGXs/s/xBAkEAlAY3ymgx
+ 63BDJEwvOb2IaP8lDDxNsXx9XJNVvQbv5n15vNsLHbjslHfAhAbxnLQ1fLhUPqSi
+ nNp/xedE1YxutQ==
+ -----END PRIVATE KEY-----
+ KEY
+ end
+
+ before do
+ allow(Gitlab.config.pages).to receive(:enabled).and_return(true)
+ project.add_role(user, role)
+ sign_in(user)
+ project.namespace.update(owner: user)
+ allow_any_instance_of(Project).to receive(:pages_deployed?) { true }
+ end
+
+ context 'when the page_auto_ssl feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: true)
+ end
+
+ context 'when the auto SSL management is initially disabled' do
+ let(:domain) do
+ create(:pages_domain, auto_ssl_enabled: false, project: project)
+ end
+
+ it 'enables auto SSL and dynamically updates the form accordingly', :js do
+ visit edit_project_pages_domain_path(project, domain)
+
+ expect(domain.auto_ssl_enabled).to eq false
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false'
+ expect(page).to have_field 'Certificate (PEM)', type: 'textarea'
+ expect(page).to have_field 'Key (PEM)', type: 'textarea'
+
+ find('.js-auto-ssl-toggle-container .project-feature-toggle').click
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true'
+ expect(page).not_to have_field 'Certificate (PEM)', type: 'textarea'
+ expect(page).not_to have_field 'Key (PEM)', type: 'textarea'
+ expect(page).to have_content "The certificate will be shown here once it has been obtained from Let's Encrypt. This process may take up to an hour to complete."
+
+ click_on 'Save Changes'
+
+ expect(domain.reload.auto_ssl_enabled).to eq true
+ end
+ end
+
+ context 'when the auto SSL management is initially enabled' do
+ let(:domain) do
+ create(:pages_domain, auto_ssl_enabled: true, project: project)
+ end
+
+ it 'disables auto SSL and dynamically updates the form accordingly', :js do
+ visit edit_project_pages_domain_path(project, domain)
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'true'
+ expect(page).to have_field 'Certificate (PEM)', type: 'textarea', disabled: true
+ expect(page).not_to have_field 'Key (PEM)', type: 'textarea'
+
+ find('.js-auto-ssl-toggle-container .project-feature-toggle').click
+
+ expect(find("#pages_domain_auto_ssl_enabled", visible: false).value).to eq 'false'
+ expect(page).to have_field 'Certificate (PEM)', type: 'textarea'
+ expect(page).to have_field 'Key (PEM)', type: 'textarea'
+
+ fill_in 'Certificate (PEM)', with: certificate_pem
+ fill_in 'Key (PEM)', with: certificate_key
+
+ click_on 'Save Changes'
+
+ expect(domain.reload.auto_ssl_enabled).to eq false
+ end
+ end
+ end
+
+ context 'when the page_auto_ssl feature flag is disabled' do
+ let(:domain) do
+ create(:pages_domain, auto_ssl_enabled: false, project: project)
+ end
+
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+
+ visit edit_project_pages_domain_path(project, domain)
+ end
+
+ it "does not render the Let's Encrypt field", :js do
+ expect(page).not_to have_selector '.js-auto-ssl-toggle-container'
+ end
+ end
+end
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index 72faeba92ee..9bb0ba81ef5 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: true
require 'spec_helper'
-describe 'Pages' do
+shared_examples 'pages domain editing' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:role) { :maintainer }
@@ -212,7 +213,7 @@ describe 'Pages' do
it 'tries to change the setting' do
visit project_pages_path(project)
- expect(page).to have_content("Force domains with SSL certificates to use HTTPS")
+ expect(page).to have_content("Force HTTPS (requires valid certificates)")
uncheck :project_pages_https_only
@@ -261,7 +262,7 @@ describe 'Pages' do
visit project_pages_path(project)
expect(page).not_to have_field(:project_pages_https_only)
- expect(page).not_to have_content('Force domains with SSL certificates to use HTTPS')
+ expect(page).not_to have_content('Force HTTPS (requires valid certificates)')
expect(page).not_to have_button('Save')
end
end
@@ -287,10 +288,17 @@ describe 'Pages' do
:ci_build,
project: project,
pipeline: pipeline,
- ref: 'HEAD',
- legacy_artifacts_file: fixture_file_upload(File.join('spec/fixtures/pages.zip')),
- legacy_artifacts_metadata: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta'))
- )
+ ref: 'HEAD')
+ end
+
+ let!(:artifact) do
+ create(:ci_job_artifact, :archive,
+ file: fixture_file_upload(File.join('spec/fixtures/pages.zip')), job: ci_build)
+ end
+
+ let!(:metadata) do
+ create(:ci_job_artifact, :metadata,
+ file: fixture_file_upload(File.join('spec/fixtures/pages.zip.meta')), job: ci_build)
end
before do
@@ -311,3 +319,21 @@ describe 'Pages' do
end
end
end
+
+describe 'Pages' do
+ context 'when pages_auto_ssl feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ end
+
+ include_examples 'pages domain editing'
+ end
+
+ context 'when pages_auto_ssl feature flag is enabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: true)
+ end
+
+ include_examples 'pages domain editing'
+ end
+end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index ee6b67b2188..24041a51383 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -93,14 +93,14 @@ describe 'Pipeline Schedules', :js do
expect(page).to have_button('UTC')
end
- it 'it creates a new scheduled pipeline' do
+ it 'creates a new scheduled pipeline' do
fill_in_schedule_form
save_pipeline_schedule
expect(page).to have_content('my fancy description')
end
- it 'it prevents an invalid form from being submitted' do
+ it 'prevents an invalid form from being submitted' do
save_pipeline_schedule
expect(page).to have_content('This field is required')
@@ -112,7 +112,7 @@ describe 'Pipeline Schedules', :js do
edit_pipeline_schedule
end
- it 'it displays existing properties' do
+ it 'displays existing properties' do
description = find_field('schedule_description').value
expect(description).to eq('pipeline schedule')
expect(page).to have_button('master')
@@ -225,7 +225,7 @@ describe 'Pipeline Schedules', :js do
context 'when active is true and next_run_at is NULL' do
before do
create(:ci_pipeline_schedule, project: project, owner: user).tap do |pipeline_schedule|
- pipeline_schedule.update_attribute(:cron, nil) # Consequently next_run_at will be nil
+ pipeline_schedule.update_attribute(:next_run_at, nil) # Consequently next_run_at will be nil
end
end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 72ef460d315..1de153db41c 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -1,6 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Pipeline', :js do
+ include RoutesHelpers
+ include ProjectForksHelper
+
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:role) { :developer }
@@ -21,6 +26,11 @@ describe 'Pipeline', :js do
pipeline: pipeline, stage: 'test', name: 'test')
end
+ let!(:build_preparing) do
+ create(:ci_build, :preparing,
+ pipeline: pipeline, stage: 'deploy', name: 'prepare')
+ end
+
let!(:build_running) do
create(:ci_build, :running,
pipeline: pipeline, stage: 'deploy', name: 'deploy')
@@ -51,11 +61,11 @@ describe 'Pipeline', :js do
let(:project) { create(:project, :repository) }
let(:pipeline) { create(:ci_pipeline, project: project, ref: 'master', sha: project.commit.id, user: user) }
- before do
- visit project_pipeline_path(project, pipeline)
- end
+ subject(:visit_pipeline) { visit project_pipeline_path(project, pipeline) }
it 'shows the pipeline graph' do
+ visit_pipeline
+
expect(page).to have_selector('.pipeline-visualization')
expect(page).to have_content('Build')
expect(page).to have_content('Test')
@@ -65,13 +75,28 @@ describe 'Pipeline', :js do
end
it 'shows Pipeline tab pane as active' do
+ visit_pipeline
+
expect(page).to have_css('#js-tab-pipeline.active')
end
it 'shows link to the pipeline ref' do
+ visit_pipeline
+
expect(page).to have_link(pipeline.ref)
end
+ it 'shows the pipeline information' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).to have_content("#{pipeline.statuses.count} jobs " \
+ "for #{pipeline.ref} ")
+ expect(page).to have_link(pipeline.ref,
+ href: project_commits_path(pipeline.project, pipeline.ref))
+ end
+ end
+
it_behaves_like 'showing user status' do
let(:user_with_status) { pipeline.user }
@@ -79,6 +104,10 @@ describe 'Pipeline', :js do
end
describe 'pipeline graph' do
+ before do
+ visit_pipeline
+ end
+
context 'when pipeline has running builds' do
it 'shows a running icon and a cancel action for the running build' do
page.within('#ci-badge-deploy') do
@@ -97,6 +126,24 @@ describe 'Pipeline', :js do
end
end
+ context 'when pipeline has preparing builds' do
+ it 'shows a preparing icon and a cancel action' do
+ page.within('#ci-badge-prepare') do
+ expect(page).to have_selector('.js-ci-status-icon-preparing')
+ expect(page).to have_selector('.js-icon-cancel')
+ expect(page).to have_content('prepare')
+ end
+ end
+
+ it 'cancels the preparing build and shows retry button' do
+ find('#ci-badge-deploy .ci-action-icon-container').click
+
+ page.within('#ci-badge-deploy') do
+ expect(page).to have_css('.js-icon-retry')
+ end
+ end
+ end
+
context 'when pipeline has successful builds' do
it 'shows the success icon and a retry action for the successful build' do
page.within('#ci-badge-build') do
@@ -109,7 +156,7 @@ describe 'Pipeline', :js do
end
end
- it 'should be possible to retry the success job' do
+ it 'is possible to retry the success job' do
find('#ci-badge-build .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
@@ -149,13 +196,13 @@ describe 'Pipeline', :js do
end
end
- it 'should be possible to retry the failed build' do
+ it 'is possible to retry the failed build' do
find('#ci-badge-test .ci-action-icon-container').click
expect(page).not_to have_content('Retry job')
end
- it 'should include the failure reason' do
+ it 'includes the failure reason' do
page.within('#ci-badge-test') do
build_link = page.find('.js-pipeline-graph-job-link')
expect(build_link['data-original-title']).to eq('test - failed - (unknown failure)')
@@ -175,7 +222,7 @@ describe 'Pipeline', :js do
end
end
- it 'should be possible to play the manual job' do
+ it 'is possible to play the manual job' do
find('#ci-badge-manual-build .ci-action-icon-container').click
expect(page).not_to have_content('Play job')
@@ -191,7 +238,25 @@ describe 'Pipeline', :js do
end
end
+ context 'when the pipeline has manual stage' do
+ before do
+ create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'CentOS')
+ create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'Debian')
+ create(:ci_build, :manual, pipeline: pipeline, stage: 'publish', name: 'OpenSUDE')
+
+ visit_pipeline
+ end
+
+ it 'displays play all button' do
+ expect(page).to have_selector('.js-stage-action')
+ end
+ end
+
context 'page tabs' do
+ before do
+ visit_pipeline
+ end
+
it 'shows Pipeline, Jobs and Failed Jobs tabs with link' do
expect(page).to have_link('Pipeline')
expect(page).to have_link('Jobs')
@@ -218,6 +283,10 @@ describe 'Pipeline', :js do
end
context 'retrying jobs' do
+ before do
+ visit_pipeline
+ end
+
it { expect(page).not_to have_content('retried') }
context 'when retrying' do
@@ -230,6 +299,10 @@ describe 'Pipeline', :js do
end
context 'canceling jobs' do
+ before do
+ visit_pipeline
+ end
+
it { expect(page).not_to have_selector('.ci-canceled') }
context 'when canceling' do
@@ -249,10 +322,161 @@ describe 'Pipeline', :js do
user: user)
end
+ before do
+ visit_pipeline
+ end
+
it 'does not render link to the pipeline ref' do
expect(page).not_to have_link(pipeline.ref)
expect(page).to have_content(pipeline.ref)
end
+
+ it 'does not render render raw HTML to the pipeline ref' do
+ page.within '.pipeline-info' do
+ expect(page).not_to have_content('<span class="ref-name"')
+ end
+ end
+ end
+
+ context 'when pipeline is detached merge request pipeline' do
+ let(:source_project) { project }
+ let(:target_project) { project }
+
+ let(:merge_request) do
+ create(:merge_request,
+ :with_detached_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project)
+ end
+
+ let(:pipeline) do
+ merge_request.all_pipelines.last
+ end
+
+ it 'shows the pipeline information' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).to have_content("#{pipeline.statuses.count} jobs " \
+ "for !#{merge_request.iid} " \
+ "with #{merge_request.source_branch}")
+ expect(page).to have_link("!#{merge_request.iid}",
+ href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.source_branch,
+ href: project_commits_path(merge_request.source_project, merge_request.source_branch))
+ end
+ end
+
+ context 'when source branch does not exist' do
+ before do
+ project.repository.rm_branch(user, merge_request.source_branch)
+ end
+
+ it 'does not link to the source branch commit path' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).not_to have_link(merge_request.source_branch)
+ expect(page).to have_content(merge_request.source_branch)
+ end
+ end
+ end
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+
+ before do
+ visit project_pipeline_path(source_project, pipeline)
+ end
+
+ it 'shows the pipeline information' do
+ within '.pipeline-info' do
+ expect(page).to have_content("#{pipeline.statuses.count} jobs " \
+ "for !#{merge_request.iid} " \
+ "with #{merge_request.source_branch}")
+ expect(page).to have_link("!#{merge_request.iid}",
+ href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.source_branch,
+ href: project_commits_path(merge_request.source_project, merge_request.source_branch))
+ end
+ end
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:source_project) { project }
+ let(:target_project) { project }
+
+ let(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project,
+ merge_sha: project.commit.id)
+ end
+
+ let(:pipeline) do
+ merge_request.all_pipelines.last
+ end
+
+ before do
+ pipeline.update(user: user)
+ end
+
+ it 'shows the pipeline information' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).to have_content("#{pipeline.statuses.count} jobs " \
+ "for !#{merge_request.iid} " \
+ "with #{merge_request.source_branch} " \
+ "into #{merge_request.target_branch}")
+ expect(page).to have_link("!#{merge_request.iid}",
+ href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.source_branch,
+ href: project_commits_path(merge_request.source_project, merge_request.source_branch))
+ expect(page).to have_link(merge_request.target_branch,
+ href: project_commits_path(merge_request.target_project, merge_request.target_branch))
+ end
+ end
+
+ context 'when target branch does not exist' do
+ before do
+ project.repository.rm_branch(user, merge_request.target_branch)
+ end
+
+ it 'does not link to the target branch commit path' do
+ visit_pipeline
+
+ within '.pipeline-info' do
+ expect(page).not_to have_link(merge_request.target_branch)
+ expect(page).to have_content(merge_request.target_branch)
+ end
+ end
+ end
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+
+ before do
+ visit project_pipeline_path(source_project, pipeline)
+ end
+
+ it 'shows the pipeline information' do
+ within '.pipeline-info' do
+ expect(page).to have_content("#{pipeline.statuses.count} jobs " \
+ "for !#{merge_request.iid} " \
+ "with #{merge_request.source_branch} " \
+ "into #{merge_request.target_branch}")
+ expect(page).to have_link("!#{merge_request.iid}",
+ href: project_merge_request_path(project, merge_request))
+ expect(page).to have_link(merge_request.source_branch,
+ href: project_commits_path(merge_request.source_project, merge_request.source_branch))
+ expect(page).to have_link(merge_request.target_branch,
+ href: project_commits_path(merge_request.target_project, merge_request.target_branch))
+ end
+ end
+ end
end
end
@@ -280,7 +504,7 @@ describe 'Pipeline', :js do
expect(page).to have_content('Cancel running')
end
- it 'should not link to job' do
+ it 'does not link to job' do
expect(page).not_to have_selector('.js-pipeline-graph-job-link')
end
end
@@ -666,7 +890,7 @@ describe 'Pipeline', :js do
let(:pipeline) do
create(:ci_pipeline,
- source: :merge_request,
+ source: :merge_request_event,
project: merge_request.source_project,
ref: 'feature',
sha: merge_request.diff_head_sha,
@@ -686,9 +910,9 @@ describe 'Pipeline', :js do
visit project_pipeline_path(project, pipeline)
end
- it 'contains badge that indicates merge request pipeline' do
+ it 'contains badge that indicates detached merge request pipeline' do
page.within(all('.well-segment')[1]) do
- expect(page).to have_content 'merge request'
+ expect(page).to have_content 'detached'
end
end
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index ffa165c5440..de780f13681 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Pipelines', :js do
+ include ProjectForksHelper
+
let(:project) { create(:project) }
context 'when user is logged in' do
@@ -165,6 +167,99 @@ describe 'Pipelines', :js do
end
end
+ context 'when pipeline is detached merge request pipeline' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_detached_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project)
+ end
+
+ let!(:pipeline) { merge_request.all_pipelines.first }
+ let(:source_project) { project }
+ let(:target_project) { project }
+
+ before do
+ visit project_pipelines_path(source_project)
+ end
+
+ shared_examples_for 'showing detached merge request pipeline information' do
+ it 'shows detached tag for the pipeline' do
+ within '.pipeline-tags' do
+ expect(page).to have_content('detached')
+ end
+ end
+
+ it 'shows the link of the merge request' do
+ within '.branch-commit' do
+ expect(page).to have_link(merge_request.iid,
+ href: project_merge_request_path(project, merge_request))
+ end
+ end
+
+ it 'does not show the ref of the pipeline' do
+ within '.branch-commit' do
+ expect(page).not_to have_link(pipeline.ref)
+ end
+ end
+ end
+
+ it_behaves_like 'showing detached merge request pipeline information'
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+
+ it_behaves_like 'showing detached merge request pipeline information'
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ source_project: source_project,
+ target_project: target_project,
+ merge_sha: target_project.commit.sha)
+ end
+
+ let!(:pipeline) { merge_request.all_pipelines.first }
+ let(:source_project) { project }
+ let(:target_project) { project }
+
+ before do
+ visit project_pipelines_path(source_project)
+ end
+
+ shared_examples_for 'Correct merge request pipeline information' do
+ it 'does not show detached tag for the pipeline' do
+ within '.pipeline-tags' do
+ expect(page).not_to have_content('detached')
+ end
+ end
+
+ it 'shows the link of the merge request' do
+ within '.branch-commit' do
+ expect(page).to have_link(merge_request.iid,
+ href: project_merge_request_path(project, merge_request))
+ end
+ end
+
+ it 'does not show the ref of the pipeline' do
+ within '.branch-commit' do
+ expect(page).not_to have_link(pipeline.ref)
+ end
+ end
+ end
+
+ it_behaves_like 'Correct merge request pipeline information'
+
+ context 'when source project is a forked project' do
+ let(:source_project) { fork_project(project, user, repository: true) }
+
+ it_behaves_like 'Correct merge request pipeline information'
+ end
+ end
+
context 'when pipeline has configuration errors' do
let(:pipeline) do
create(:ci_pipeline, :invalid, project: project)
@@ -282,6 +377,30 @@ describe 'Pipelines', :js do
end
context 'for generic statuses' do
+ context 'when preparing' do
+ let!(:pipeline) do
+ create(:ci_empty_pipeline,
+ status: 'preparing', project: project)
+ end
+
+ let!(:status) do
+ create(:generic_commit_status,
+ :preparing, pipeline: pipeline)
+ end
+
+ before do
+ visit_project_pipelines
+ end
+
+ it 'is cancelable' do
+ expect(page).to have_selector('.js-pipelines-cancel-button')
+ end
+
+ it 'shows the pipeline as preparing' do
+ expect(page).to have_selector('.ci-preparing')
+ end
+ end
+
context 'when running' do
let!(:running) do
create(:generic_commit_status,
@@ -423,19 +542,19 @@ describe 'Pipelines', :js do
visit_project_pipelines
end
- it 'should render a mini pipeline graph' do
+ it 'renders a mini pipeline graph' do
expect(page).to have_selector('.js-mini-pipeline-graph')
expect(page).to have_selector('.js-builds-dropdown-button')
end
context 'when clicking a stage badge' do
- it 'should open a dropdown' do
+ it 'opens a dropdown' do
find('.js-builds-dropdown-button').click
expect(page).to have_link build.name
end
- it 'should be possible to cancel pending build' do
+ it 'is possible to cancel pending build' do
find('.js-builds-dropdown-button').click
find('.js-ci-action').click
wait_for_requests
@@ -451,7 +570,7 @@ describe 'Pipelines', :js do
name: 'build')
end
- it 'should display the failure reason' do
+ it 'displays the failure reason' do
find('.js-builds-dropdown-button').click
within('.js-builds-dropdown-list') do
@@ -468,24 +587,24 @@ describe 'Pipelines', :js do
create(:ci_empty_pipeline, project: project)
end
- it 'should render pagination' do
+ it 'renders pagination' do
visit project_pipelines_path(project)
wait_for_requests
expect(page).to have_selector('.gl-pagination')
end
- it 'should render second page of pipelines' do
+ it 'renders second page of pipelines' do
visit project_pipelines_path(project, page: '2')
wait_for_requests
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
- it 'should show updated content' do
+ it 'shows updated content' do
visit project_pipelines_path(project)
wait_for_requests
- page.find('.js-next-button a').click
+ page.find('.js-next-button .page-link').click
expect(page).to have_selector('.gl-pagination .page', count: 2)
end
@@ -566,7 +685,7 @@ describe 'Pipelines', :js do
end
it 'creates a new pipeline' do
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run Pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last).to be_web
@@ -579,7 +698,7 @@ describe 'Pipelines', :js do
fill_in "Input variable value", with: "value"
end
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run Pipeline' }
.to change { Ci::Pipeline.count }.by(1)
expect(Ci::Pipeline.last.variables.map { |var| var.slice(:key, :secret_value) })
@@ -590,7 +709,7 @@ describe 'Pipelines', :js do
context 'without gitlab-ci.yml' do
before do
- click_on 'Create pipeline'
+ click_on 'Run Pipeline'
end
it { expect(page).to have_content('Missing .gitlab-ci.yml file') }
@@ -603,14 +722,14 @@ describe 'Pipelines', :js do
click_link 'master'
end
- expect { click_on 'Create pipeline' }
+ expect { click_on 'Run Pipeline' }
.to change { Ci::Pipeline.count }.by(1)
end
end
end
end
- describe 'Create pipelines' do
+ describe 'Run Pipelines' do
let(:project) { create(:project, :repository) }
before do
@@ -621,7 +740,7 @@ describe 'Pipelines', :js do
it 'has field to add a new pipeline' do
expect(page).to have_selector('.js-branch-select')
expect(find('.js-branch-select')).to have_content project.default_branch
- expect(page).to have_content('Create for')
+ expect(page).to have_content('Run for')
end
end
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index aa71669de98..9865dbbfb3c 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe 'Functions', :js do
include KubernetesHelpers
+ include ReactiveCachingHelpers
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -13,44 +14,70 @@ describe 'Functions', :js do
gitlab_sign_in(user)
end
- context 'when user does not have a cluster and visits the serverless page' do
+ shared_examples "it's missing knative installation" do
before do
visit project_serverless_functions_path(project)
end
- it 'sees an empty state' do
+ it 'sees an empty state require Knative installation' do
expect(page).to have_link('Install Knative')
expect(page).to have_selector('.empty-state')
end
end
+ context 'when user does not have a cluster and visits the serverless page' do
+ it_behaves_like "it's missing knative installation"
+ end
+
context 'when the user does have a cluster and visits the serverless page' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- before do
- visit project_serverless_functions_path(project)
- end
-
- it 'sees an empty state' do
- expect(page).to have_link('Install Knative')
- expect(page).to have_selector('.empty-state')
- end
+ it_behaves_like "it's missing knative installation"
end
context 'when the user has a cluster and knative installed and visits the serverless page' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
- let(:project) { knative.cluster.project }
+ let(:project) { cluster.project }
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: cluster.cluster_project.project)
+ end
before do
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ stub_kubeclient_knative_services(stub_get_services_options)
+ stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
visit project_serverless_functions_path(project)
end
- it 'sees an empty listing of serverless functions' do
- expect(page).to have_selector('.gl-responsive-table-row')
+ context 'when there are no functions' do
+ let(:stub_get_services_options) do
+ {
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ }
+ end
+
+ it 'sees an empty listing of serverless functions' do
+ expect(page).to have_selector('.empty-state')
+ expect(page).not_to have_selector('.content-list')
+ end
+ end
+
+ context 'when there are functions' do
+ let(:stub_get_services_options) { { namespace: namespace.namespace } }
+
+ it 'does not see an empty listing of serverless functions' do
+ expect(page).not_to have_selector('.empty-state')
+ expect(page).to have_selector('.content-list')
+ end
end
end
end
diff --git a/spec/features/projects/services/disable_triggers_spec.rb b/spec/features/projects/services/disable_triggers_spec.rb
index 65b597da269..1a13fe03a67 100644
--- a/spec/features/projects/services/disable_triggers_spec.rb
+++ b/spec/features/projects/services/disable_triggers_spec.rb
@@ -14,11 +14,10 @@ describe 'Disable individual triggers' do
end
context 'service has multiple supported events' do
- let(:service_name) { 'JIRA' }
+ let(:service_name) { 'HipChat' }
it 'shows trigger checkboxes' do
- event_count = JiraService.supported_events.count
- expect(event_count).to be > 1
+ event_count = HipchatService.supported_events.count
expect(page).to have_content "Trigger"
expect(page).to have_css(checkbox_selector, count: event_count)
diff --git a/spec/features/projects/services/user_activates_hipchat_spec.rb b/spec/features/projects/services/user_activates_hipchat_spec.rb
new file mode 100644
index 00000000000..d6b69a5bd68
--- /dev/null
+++ b/spec/features/projects/services/user_activates_hipchat_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User activates HipChat' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('HipChat')
+ end
+
+ context 'with standart settings' do
+ it 'activates service' do
+ check('Active')
+ fill_in('Room', with: 'gitlab')
+ fill_in('Token', with: 'verySecret')
+ click_button('Save')
+
+ expect(page).to have_content('HipChat activated.')
+ end
+ end
+
+ context 'with custom settings' do
+ it 'activates service' do
+ check('Active')
+ fill_in('Room', with: 'gitlab_custom')
+ fill_in('Token', with: 'secretCustom')
+ fill_in('Server', with: 'https://chat.example.com')
+ click_button('Save')
+
+ expect(page).to have_content('HipChat activated.')
+ end
+ end
+end
diff --git a/spec/features/projects/services/user_activates_issue_tracker_spec.rb b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
index 7cd5b12802b..74b9a2b20cd 100644
--- a/spec/features/projects/services/user_activates_issue_tracker_spec.rb
+++ b/spec/features/projects/services/user_activates_issue_tracker_spec.rb
@@ -6,11 +6,17 @@ describe 'User activates issue tracker', :js do
let(:url) { 'http://tracker.example.com' }
- def fill_form(active = true)
+ def fill_short_form(active = true)
check 'Active' if active
fill_in 'service_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id"
+ end
+
+ def fill_full_form(active = true)
+ fill_short_form(active)
+ check 'Active' if active
+
fill_in 'service_new_issue_url', with: url
end
@@ -21,14 +27,20 @@ describe 'User activates issue tracker', :js do
visit project_settings_integrations_path(project)
end
- shared_examples 'external issue tracker activation' do |tracker:|
+ shared_examples 'external issue tracker activation' do |tracker:, skip_new_issue_url: false|
describe 'user sets and activates the Service' do
context 'when the connection test succeeds' do
before do
stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
click_link(tracker)
- fill_form
+
+ if skip_new_issue_url
+ fill_short_form
+ else
+ fill_full_form
+ end
+
click_button('Test settings and save changes')
wait_for_requests
end
@@ -50,7 +62,13 @@ describe 'User activates issue tracker', :js do
stub_request(:head, url).to_raise(HTTParty::Error)
click_link(tracker)
- fill_form
+
+ if skip_new_issue_url
+ fill_short_form
+ else
+ fill_full_form
+ end
+
click_button('Test settings and save changes')
wait_for_requests
@@ -69,7 +87,13 @@ describe 'User activates issue tracker', :js do
describe 'user sets the service but keeps it disabled' do
before do
click_link(tracker)
- fill_form(false)
+
+ if skip_new_issue_url
+ fill_short_form(false)
+ else
+ fill_full_form(false)
+ end
+
click_button('Save changes')
end
@@ -87,6 +111,7 @@ describe 'User activates issue tracker', :js do
end
it_behaves_like 'external issue tracker activation', tracker: 'Redmine'
+ it_behaves_like 'external issue tracker activation', tracker: 'YouTrack', skip_new_issue_url: true
it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla'
it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker'
end
diff --git a/spec/features/projects/services/user_activates_youtrack_spec.rb b/spec/features/projects/services/user_activates_youtrack_spec.rb
new file mode 100644
index 00000000000..bb6a030c1cf
--- /dev/null
+++ b/spec/features/projects/services/user_activates_youtrack_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe 'User activates issue tracker', :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:url) { 'http://tracker.example.com' }
+
+ def fill_form(active = true)
+ check 'Active' if active
+
+ fill_in 'service_project_url', with: url
+ fill_in 'service_issues_url', with: "#{url}/:id"
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+
+ visit project_settings_integrations_path(project)
+ end
+
+ shared_examples 'external issue tracker activation' do |tracker:|
+ describe 'user sets and activates the Service' do
+ context 'when the connection test succeeds' do
+ before do
+ stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
+
+ click_link(tracker)
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+ end
+
+ it 'activates the service' do
+ expect(page).to have_content("#{tracker} activated.")
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+
+ it 'shows the link in the menu' do
+ page.within('.nav-sidebar') do
+ expect(page).to have_link(tracker, href: url)
+ end
+ end
+ end
+
+ context 'when the connection test fails' do
+ it 'activates the service' do
+ stub_request(:head, url).to_raise(HTTParty::Error)
+
+ click_link(tracker)
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(find('.flash-container-page')).to have_content 'Test failed.'
+ expect(find('.flash-container-page')).to have_content 'Save anyway'
+
+ find('.flash-alert .flash-action').click
+ wait_for_requests
+
+ expect(page).to have_content("#{tracker} activated.")
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+ end
+ end
+
+ describe 'user sets the service but keeps it disabled' do
+ before do
+ click_link(tracker)
+ fill_form(false)
+ click_button('Save changes')
+ end
+
+ it 'saves but does not activate the service' do
+ expect(page).to have_content("#{tracker} settings saved, but not activated.")
+ expect(current_path).to eq(project_settings_integrations_path(project))
+ end
+
+ it 'does not show the external tracker link in the menu' do
+ page.within('.nav-sidebar') do
+ expect(page).not_to have_link(tracker, href: url)
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'external issue tracker activation', tracker: 'YouTrack'
+end
diff --git a/spec/features/projects/services/user_views_services_spec.rb b/spec/features/projects/services/user_views_services_spec.rb
index b0a838a7d2b..e9c8cf0fe34 100644
--- a/spec/features/projects/services/user_views_services_spec.rb
+++ b/spec/features/projects/services/user_views_services_spec.rb
@@ -14,6 +14,7 @@ describe 'User views services' do
it 'shows the list of available services' do
expect(page).to have_content('Project services')
expect(page).to have_content('Campfire')
+ expect(page).to have_content('HipChat')
expect(page).to have_content('Assembla')
expect(page).to have_content('Pushover')
expect(page).to have_content('Atlassian Bamboo')
@@ -21,7 +22,5 @@ describe 'User views services' do
expect(page).to have_content('Asana')
expect(page).to have_content('Irker (IRC gateway)')
expect(page).to have_content('Packagist')
- expect(page).to have_content('Mattermost')
- expect(page).to have_content('Slack')
end
end
diff --git a/spec/features/projects/settings/external_authorization_service_settings_spec.rb b/spec/features/projects/settings/external_authorization_service_settings_spec.rb
new file mode 100644
index 00000000000..31b2892cf6f
--- /dev/null
+++ b/spec/features/projects/settings/external_authorization_service_settings_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Projects > Settings > External Authorization Classification Label setting' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project_empty_repo) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'shows the field to set a classification label' do
+ stub_application_setting(external_authorization_service_enabled: true)
+
+ visit edit_project_path(project)
+
+ expect(page).to have_selector('#project_external_authorization_classification_label')
+ end
+end
diff --git a/spec/features/projects/settings/forked_project_settings_spec.rb b/spec/features/projects/settings/forked_project_settings_spec.rb
index dc0278370aa..df33d215602 100644
--- a/spec/features/projects/settings/forked_project_settings_spec.rb
+++ b/spec/features/projects/settings/forked_project_settings_spec.rb
@@ -7,7 +7,6 @@ describe 'Projects > Settings > For a forked project', :js do
let(:forked_project) { fork_project(original_project, user) }
before do
- stub_feature_flags(approval_rules: false)
original_project.add_maintainer(user)
forked_project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects/settings/operations_settings_spec.rb b/spec/features/projects/settings/operations_settings_spec.rb
index 06290c67c70..d96e243d96b 100644
--- a/spec/features/projects/settings/operations_settings_spec.rb
+++ b/spec/features/projects/settings/operations_settings_spec.rb
@@ -20,4 +20,87 @@ describe 'Projects > Settings > For a forked project', :js do
expect(page).to have_selector('a[title="Operations"]', visible: false)
end
end
+
+ describe 'Settings > Operations' do
+ context 'error tracking settings form' do
+ let(:sentry_list_projects_url) { 'http://sentry.example.com/api/0/projects/' }
+
+ context 'success path' do
+ let(:projects_sample_response) do
+ Gitlab::Utils.deep_indifferent_access(
+ JSON.parse(fixture_file('sentry/list_projects_sample_response.json'))
+ )
+ end
+
+ before do
+ WebMock.stub_request(:get, sentry_list_projects_url)
+ .to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: projects_sample_response.to_json
+ )
+ end
+
+ it 'successfully fills and submits the form' do
+ visit project_settings_operations_path(project)
+
+ wait_for_requests
+
+ within '.js-error-tracking-settings' do
+ click_button('Expand')
+ end
+ expect(page).to have_content('Sentry API URL')
+ expect(page.body).to include('Error Tracking')
+ expect(page).to have_button('Connect')
+
+ check('Active')
+ fill_in('error-tracking-api-host', with: 'http://sentry.example.com')
+ fill_in('error-tracking-token', with: 'token')
+
+ click_button('Connect')
+
+ within('div#project-dropdown') do
+ click_button('Select project')
+ click_button('Sentry | Internal')
+ end
+
+ click_button('Save changes')
+
+ wait_for_requests
+
+ assert_text('Your changes have been saved')
+ end
+ end
+
+ context 'project dropdown fails to load' do
+ before do
+ WebMock.stub_request(:get, sentry_list_projects_url)
+ .to_return(
+ status: 400,
+ headers: { 'Content-Type' => 'application/json' },
+ body: {
+ message: 'Sentry response code: 401'
+ }.to_json
+ )
+ end
+
+ it 'displays error message' do
+ visit project_settings_operations_path(project)
+
+ wait_for_requests
+
+ within '.js-error-tracking-settings' do
+ click_button('Expand')
+ end
+ check('Active')
+ fill_in('error-tracking-api-host', with: 'http://sentry.example.com')
+ fill_in('error-tracking-token', with: 'token')
+
+ click_button('Connect')
+
+ assert_text('Connection has failed. Re-check Auth Token and try again.')
+ end
+ end
+ end
+ end
end
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 4c85abe9971..bf0c0de89b2 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -110,6 +110,37 @@ describe "Projects > Settings > Pipelines settings" do
expect(page).not_to have_content('instance enabled')
end
end
+
+ context 'when auto devops is turned on group level' do
+ before do
+ project.update!(namespace: create(:group, :auto_devops_enabled))
+ end
+
+ it 'renders group enabled badge' do
+ visit project_settings_ci_cd_path(project)
+
+ page.within '#autodevops-settings' do
+ expect(page).to have_content('group enabled')
+ expect(find_field('project_auto_devops_attributes_enabled')).to be_checked
+ end
+ end
+ end
+
+ context 'when auto devops is turned on group parent level', :nested_groups do
+ before do
+ group = create(:group, parent: create(:group, :auto_devops_enabled))
+ project.update!(namespace: group)
+ end
+
+ it 'renders group enabled badge' do
+ visit project_settings_ci_cd_path(project)
+
+ page.within '#autodevops-settings' do
+ expect(page).to have_content('group enabled')
+ expect(find_field('project_auto_devops_attributes_enabled')).to be_checked
+ end
+ end
+ end
end
end
diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb
new file mode 100644
index 00000000000..7afddc0e712
--- /dev/null
+++ b/spec/features/projects/settings/project_settings_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Projects settings' do
+ set(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:panel) { find('.general-settings', match: :first) }
+ let(:button) { panel.find('.btn.js-settings-toggle') }
+ let(:title) { panel.find('.settings-title') }
+
+ before do
+ sign_in(user)
+ visit edit_project_path(project)
+ end
+
+ it 'can toggle sections by clicking the title or button', :js do
+ expect_toggle_state(:expanded)
+
+ button.click
+
+ expect_toggle_state(:collapsed)
+
+ button.click
+
+ expect_toggle_state(:expanded)
+
+ title.click
+
+ expect_toggle_state(:collapsed)
+
+ title.click
+
+ expect_toggle_state(:expanded)
+ end
+
+ def expect_toggle_state(state)
+ is_collapsed = state == :collapsed
+
+ expect(button).to have_content(is_collapsed ? 'Expand' : 'Collapse')
+ expect(panel[:class]).send(is_collapsed ? 'not_to' : 'to', include('expanded'))
+ end
+end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index 1259ad45791..8c7bc192c50 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -217,5 +217,36 @@ describe 'Projects > Settings > Repository settings' do
expect(RepositoryCleanupWorker.jobs.count).to eq(1)
end
end
+
+ context 'with an existing mirror', :js do
+ let(:mirrored_project) { create(:project, :repository, :remote_mirror) }
+
+ before do
+ mirrored_project.add_maintainer(user)
+
+ visit project_settings_repository_path(mirrored_project)
+ end
+
+ it 'delete remote mirrors' do
+ expect(mirrored_project.remote_mirrors.count).to eq(1)
+
+ find('.js-delete-mirror').click
+ wait_for_requests
+
+ expect(mirrored_project.remote_mirrors.count).to eq(0)
+ end
+ end
+
+ it 'shows a disabled mirror' do
+ create(:remote_mirror, project: project, enabled: false)
+
+ visit project_settings_repository_path(project)
+
+ mirror = find('.qa-mirrored-repository-row')
+
+ expect(mirror).to have_selector('.qa-delete-mirror')
+ expect(mirror).to have_selector('.qa-disabled-mirror-badge')
+ expect(mirror).not_to have_selector('.qa-update-now-button')
+ end
end
end
diff --git a/spec/features/projects/settings/user_manages_group_links_spec.rb b/spec/features/projects/settings/user_manages_group_links_spec.rb
index 676659b90c3..e5a58c44e41 100644
--- a/spec/features/projects/settings/user_manages_group_links_spec.rb
+++ b/spec/features/projects/settings/user_manages_group_links_spec.rb
@@ -10,6 +10,7 @@ describe 'Projects > Settings > User manages group links' do
before do
project.add_maintainer(user)
+ group_market.add_guest(user)
sign_in(user)
share_link = project.project_group_links.new(group_access: Gitlab::Access::MAINTAINER)
diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
index 84de6858d5f..0739726f52c 100644
--- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
require 'spec_helper'
describe 'Projects > Settings > User manages merge request settings' do
@@ -30,16 +31,16 @@ describe 'Projects > Settings > User manages merge request settings' do
context 'when Merge Request and Pipelines are initially enabled', :js do
context 'when Pipelines are initially enabled' do
it 'shows the Merge Requests settings' do
- expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
- expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).not_to have_content 'Pipelines must succeed'
+ expect(page).not_to have_content 'All discussions must be resolved'
end
end
@@ -50,16 +51,16 @@ describe 'Projects > Settings > User manages merge request settings' do
end
it 'shows the Merge Requests settings that do not depend on Builds feature' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
- expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All discussions must be resolved'
end
end
end
@@ -71,16 +72,16 @@ describe 'Projects > Settings > User manages merge request settings' do
end
it 'does not show the Merge Requests settings' do
- expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).not_to have_content 'Pipelines must succeed'
+ expect(page).not_to have_content 'All discussions must be resolved'
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
find('input[value="Save changes"]').send_keys(:return)
end
- expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
- expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
+ expect(page).to have_content 'Pipelines must succeed'
+ expect(page).to have_content 'All discussions must be resolved'
end
end
@@ -93,11 +94,13 @@ describe 'Projects > Settings > User manages merge request settings' do
it 'when unchecked sets :printing_merge_request_link_enabled to false' do
uncheck('project_printing_merge_request_link_enabled')
within('.merge-request-settings-form') do
+ find('.qa-save-merge-request-changes')
click_on('Save changes')
end
- # Wait for save to complete and page to reload
+ find('.flash-notice')
checkbox = find_field('project_printing_merge_request_link_enabled')
+
expect(checkbox).not_to be_checked
project.reload
diff --git a/spec/features/projects/settings/user_renames_a_project_spec.rb b/spec/features/projects/settings/user_renames_a_project_spec.rb
index 64c9af4b706..d3979b79910 100644
--- a/spec/features/projects/settings/user_renames_a_project_spec.rb
+++ b/spec/features/projects/settings/user_renames_a_project_spec.rb
@@ -9,24 +9,33 @@ describe 'Projects > Settings > User renames a project' do
visit edit_project_path(project)
end
- def rename_project(project, name: nil, path: nil)
- fill_in('project_name', with: name) if name
- fill_in('Path', with: path) if path
- click_button('Rename project')
+ def change_path(project, path)
+ within('.advanced-settings') do
+ fill_in('Path', with: path)
+ click_button('Change path')
+ end
+ project.reload
wait_for_edit_project_page_reload
+ end
+
+ def change_name(project, name)
+ within('.general-settings') do
+ fill_in('Project name', with: name)
+ click_button('Save changes')
+ end
project.reload
+ wait_for_edit_project_page_reload
end
def wait_for_edit_project_page_reload
- expect(find('.project-edit-container')).to have_content('Rename repository')
+ expect(find('.advanced-settings')).to have_content('Change path')
end
context 'with invalid characters' do
- it 'shows errors for invalid project path/name' do
- rename_project(project, name: 'foo&bar', path: 'foo&bar')
- expect(page).to have_field 'Project name', with: 'foo&bar'
+ it 'shows errors for invalid project path' do
+ change_path(project, 'foo&bar')
+
expect(page).to have_field 'Path', with: 'foo&bar'
- expect(page).to have_content "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'."
expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'"
end
end
@@ -42,13 +51,13 @@ describe 'Projects > Settings > User renames a project' do
context 'when changing project name' do
it 'renames the repository' do
- rename_project(project, name: 'bar')
+ change_name(project, 'bar')
expect(find('.breadcrumbs')).to have_content(project.name)
end
context 'with emojis' do
it 'shows error for invalid project name' do
- rename_project(project, name: '🚀 foo bar ☁️')
+ change_name(project, '🚀 foo bar ☁️')
expect(page).to have_field 'Project name', with: '🚀 foo bar ☁️'
expect(page).not_to have_content "Name can contain only letters, digits, emojis '_', '.', dash and space. It must start with letter, digit, emoji or '_'."
end
@@ -67,7 +76,7 @@ describe 'Projects > Settings > User renames a project' do
end
it 'the project is accessible via the new path' do
- rename_project(project, path: 'bar')
+ change_path(project, 'bar')
new_path = namespace_project_path(project.namespace, 'bar')
visit new_path
@@ -77,7 +86,7 @@ describe 'Projects > Settings > User renames a project' do
it 'the project is accessible via a redirect from the old path' do
old_path = project_path(project)
- rename_project(project, path: 'bar')
+ change_path(project, 'bar')
new_path = namespace_project_path(project.namespace, 'bar')
visit old_path
@@ -88,7 +97,7 @@ describe 'Projects > Settings > User renames a project' do
context 'and a new project is added with the same path' do
it 'overrides the redirect' do
old_path = project_path(project)
- rename_project(project, path: 'bar')
+ change_path(project, 'bar')
new_project = create(:project, namespace: user.namespace, path: 'gitlabhq', name: 'quz')
visit old_path
diff --git a/spec/features/projects/show/download_buttons_spec.rb b/spec/features/projects/show/download_buttons_spec.rb
index 3a2dcc5aa55..fee5f8001b0 100644
--- a/spec/features/projects/show/download_buttons_spec.rb
+++ b/spec/features/projects/show/download_buttons_spec.rb
@@ -35,11 +35,10 @@ describe 'Projects > Show > Download buttons' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{project.default_branch}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
it 'download links have download attribute' do
- expect(page).to have_selector('a', text: 'Download')
page.all('a', text: 'Download').each do |link|
expect(link[:download]).to eq ''
end
diff --git a/spec/features/projects/show/user_manages_notifications_spec.rb b/spec/features/projects/show/user_manages_notifications_spec.rb
index 88f3397608f..e9dd1dc0f66 100644
--- a/spec/features/projects/show/user_manages_notifications_spec.rb
+++ b/spec/features/projects/show/user_manages_notifications_spec.rb
@@ -20,6 +20,16 @@ describe 'Projects > Show > User manages notifications', :js do
click_notifications_button
expect(find('.update-notification.is-active')).to have_content('On mention')
+ expect(find('.notifications-icon use')[:'xlink:href']).to end_with('#notifications')
+ end
+
+ it 'changes the notification setting to disabled' do
+ click_notifications_button
+ click_link 'Disabled'
+
+ wait_for_requests
+
+ expect(find('.notifications-icon use')[:'xlink:href']).to end_with('#notifications-off')
end
context 'custom notification settings' do
diff --git a/spec/features/projects/show/user_sees_collaboration_links_spec.rb b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
index 24777788248..46586b891e7 100644
--- a/spec/features/projects/show/user_sees_collaboration_links_spec.rb
+++ b/spec/features/projects/show/user_sees_collaboration_links_spec.rb
@@ -5,6 +5,7 @@ describe 'Projects > Show > Collaboration links' do
let(:user) { create(:user) }
before do
+ stub_feature_flags(vue_file_list: false)
project.add_developer(user)
sign_in(user)
end
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index ffa80235083..0d59ef4a727 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -13,7 +13,7 @@ describe 'Projects > Show > User sees Git instructions' do
it 'shows Git command line instructions' do
click_link 'Create empty repository'
- page.within '.empty_wrapper' do
+ page.within '.empty-wrapper' do
expect(page).to have_content('Command line instructions')
end
end
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index dcca1d388c7..58bd20d7551 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -20,18 +20,18 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project)
end
- it 'no Auto DevOps button if can not manage pipelines' do
- page.within('.project-buttons') do
- expect(page).not_to have_link('Enable Auto DevOps')
- expect(page).not_to have_link('Auto DevOps enabled')
- end
- end
-
- it '"Auto DevOps enabled" button not linked' do
+ it 'Project buttons are not visible' do
visit project_path(project)
page.within('.project-buttons') do
- expect(page).to have_text('Auto DevOps enabled')
+ expect(page).not_to have_link('New file')
+ expect(page).not_to have_link('Add README')
+ expect(page).not_to have_link('Add CHANGELOG')
+ expect(page).not_to have_link('Add CONTRIBUTING')
+ expect(page).not_to have_link('Enable Auto DevOps')
+ expect(page).not_to have_link('Auto DevOps enabled')
+ expect(page).not_to have_link('Add Kubernetes cluster')
+ expect(page).not_to have_link('Kubernetes configured')
end
end
end
@@ -61,46 +61,6 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
expect(page).to have_link('Add license', href: presenter.add_license_path)
end
end
-
- describe 'Auto DevOps button' do
- context 'when Auto DevOps is enabled' do
- it '"Auto DevOps enabled" anchor linked to settings page' do
- visit project_path(project)
-
- page.within('.project-buttons') do
- expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
- end
- end
- end
-
- context 'when Auto DevOps is not enabled' do
- let(:project) { create(:project, :public, :empty_repo, auto_devops_attributes: { enabled: false }) }
-
- it '"Enable Auto DevOps" button linked to settings page' do
- page.within('.project-buttons') do
- expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
- end
- end
- end
- end
-
- describe 'Kubernetes cluster button' do
- it '"Add Kubernetes cluster" button linked to clusters page' do
- page.within('.project-buttons') do
- expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project))
- end
- end
-
- it '"Kubernetes cluster" anchor linked to cluster page' do
- cluster = create(:cluster, :provided_by_gcp, projects: [project])
-
- visit project_path(project)
-
- page.within('.project-buttons') do
- expect(page).to have_link('Kubernetes configured', href: project_cluster_path(project, cluster))
- end
- end
- end
end
end
diff --git a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
index 9c1ef78b0ca..4e1e2f330ec 100644
--- a/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
+++ b/spec/features/projects/snippets/user_comments_on_snippet_spec.rb
@@ -23,14 +23,14 @@ describe 'Projects > Snippets > User comments on a snippet', :js do
expect(page).to have_content('Good snippet!')
end
- it 'should have autocomplete' do
+ it 'has autocomplete' do
find('#note_note').native.send_keys('')
fill_in 'note[note]', with: '@'
expect(page).to have_selector('.atwho-view')
end
- it 'should have zen mode' do
+ it 'has zen mode' do
find('.js-zen-enter').click
expect(page).to have_selector('.fullscreen')
end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
index fbfd8cee7aa..4c8ec53836a 100644
--- a/spec/features/projects/tags/download_buttons_spec.rb
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -36,7 +36,7 @@ describe 'Download buttons in tags page' do
it 'shows download artifacts button' do
href = latest_succeeded_project_artifacts_path(project, "#{tag}/download", job: 'build')
- expect(page).to have_link "Download '#{build.name}'", href: href
+ expect(page).to have_link build.name, href: href
end
end
end
diff --git a/spec/features/projects/tree/tree_show_spec.rb b/spec/features/projects/tree/tree_show_spec.rb
index 45e81e1c040..3ccea2db705 100644
--- a/spec/features/projects/tree/tree_show_spec.rb
+++ b/spec/features/projects/tree/tree_show_spec.rb
@@ -8,6 +8,7 @@ describe 'Projects tree', :js do
let(:test_sha) { '7975be0116940bf2ad4321f79d02a55c5f7779aa' }
before do
+ stub_feature_flags(vue_file_list: false)
project.add_maintainer(user)
sign_in(user)
end
diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb
index 8d7e2883b2a..c0932539131 100644
--- a/spec/features/projects/user_creates_project_spec.rb
+++ b/spec/features/projects/user_creates_project_spec.rb
@@ -54,4 +54,31 @@ describe 'User creates a project', :js do
expect(project.namespace).to eq(subgroup)
end
end
+
+ context 'in a group with DEVELOPER_MAINTAINER_PROJECT_ACCESS project_creation_level' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ it 'creates a new project' do
+ visit(new_project_path)
+
+ fill_in :project_name, with: 'a-new-project'
+ fill_in :project_path, with: 'a-new-project'
+
+ page.find('.js-select-namespace').click
+ page.find("div[role='option']", text: group.full_path).click
+
+ page.within('#content-body') do
+ click_button('Create project')
+ end
+
+ expect(page).to have_content("Project 'a-new-project' was successfully created")
+
+ project = Project.find_by(name: 'a-new-project')
+ expect(project.namespace).to eq(group)
+ end
+ end
end
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index ee5734a9bf1..383e8824b7b 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -4,6 +4,108 @@ describe 'Projects > User sees sidebar' do
let(:user) { create(:user) }
let(:project) { create(:project, :private, public_builds: false, namespace: user.namespace) }
+ # NOTE: See documented behaviour https://design.gitlab.com/regions/navigation#contextual-navigation
+ context 'on different viewports', :js do
+ include MobileHelpers
+
+ before do
+ sign_in(user)
+ end
+
+ shared_examples 'has a expanded nav sidebar' do
+ it 'has a expanded desktop nav-sidebar on load' do
+ expect(page).to have_content('Collapse sidebar')
+ expect(page).not_to have_selector('.sidebar-collapsed-desktop')
+ expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ end
+
+ it 'can collapse the nav-sidebar' do
+ page.find('.nav-sidebar .js-toggle-sidebar').click
+ expect(page).to have_selector('.sidebar-collapsed-desktop')
+ expect(page).not_to have_content('Collapse sidebar')
+ expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ end
+ end
+
+ shared_examples 'has a collapsed nav sidebar' do
+ it 'has a collapsed desktop nav-sidebar on load' do
+ expect(page).not_to have_content('Collapse sidebar')
+ expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ end
+
+ it 'can expand the nav-sidebar' do
+ page.find('.nav-sidebar .js-toggle-sidebar').click
+ expect(page).to have_selector('.sidebar-expanded-mobile')
+ expect(page).to have_content('Collapse sidebar')
+ end
+ end
+
+ shared_examples 'has a mobile nav-sidebar' do
+ it 'has a hidden nav-sidebar on load' do
+ expect(page).not_to have_content('.mobile-nav-open')
+ expect(page).not_to have_selector('.sidebar-expanded-mobile')
+ end
+
+ it 'can expand the nav-sidebar' do
+ page.find('.toggle-mobile-nav').click
+ expect(page).to have_selector('.mobile-nav-open')
+ expect(page).to have_selector('.sidebar-expanded-mobile')
+ end
+ end
+
+ context 'with a extra small viewport' do
+ before do
+ resize_screen_xs
+ visit project_path(project)
+ expect(page).to have_selector('.nav-sidebar')
+ expect(page).to have_selector('.toggle-mobile-nav')
+ end
+
+ it_behaves_like 'has a mobile nav-sidebar'
+ end
+
+ context 'with a small size viewport' do
+ before do
+ resize_screen_sm
+ visit project_path(project)
+ expect(page).to have_selector('.nav-sidebar')
+ expect(page).to have_selector('.toggle-mobile-nav')
+ end
+
+ it_behaves_like 'has a mobile nav-sidebar'
+ end
+
+ context 'with medium size viewport' do
+ before do
+ resize_window(768, 800)
+ visit project_path(project)
+ expect(page).to have_selector('.nav-sidebar')
+ end
+
+ it_behaves_like 'has a collapsed nav sidebar'
+ end
+
+ context 'with viewport size 1199px' do
+ before do
+ resize_window(1199, 800)
+ visit project_path(project)
+ expect(page).to have_selector('.nav-sidebar')
+ end
+
+ it_behaves_like 'has a collapsed nav sidebar'
+ end
+
+ context 'with a extra large viewport' do
+ before do
+ resize_window(1200, 800)
+ visit project_path(project)
+ expect(page).to have_selector('.nav-sidebar')
+ end
+
+ it_behaves_like 'has a expanded nav sidebar'
+ end
+ end
+
context 'as owner' do
before do
sign_in(user)
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index b1a7f167977..aac095bfa6b 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -43,7 +43,7 @@ describe "User creates wiki page" do
expect(page).to have_content("Create Page")
end
- it "shows non-escaped link in the pages list", :js do
+ it "shows non-escaped link in the pages list", :js, :quarantine do
fill_in(:wiki_title, with: "one/two/three-test")
page.within(".wiki-form") do
@@ -136,12 +136,12 @@ describe "User creates wiki page" do
click_button("Create page")
end
- page.within ".wiki" do
+ page.within ".md" do
expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4")
end
end
- it_behaves_like 'wiki file attachments'
+ it_behaves_like 'wiki file attachments', :quarantine
end
context "in a group namespace", :js do
@@ -151,7 +151,7 @@ describe "User creates wiki page" do
expect(page).to have_field("wiki[message]", with: "Create home")
end
- it "creates a page from the home page" do
+ it "creates a page from the home page", :quarantine do
page.within(".wiki-form") do
fill_in(:wiki_content, with: "My awesome wiki!")
diff --git a/spec/features/projects/wiki/user_views_wiki_pages_spec.rb b/spec/features/projects/wiki/user_views_wiki_pages_spec.rb
new file mode 100644
index 00000000000..5c16d7783f0
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_wiki_pages_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User views wiki pages' do
+ include WikiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :wiki_repo, namespace: user.namespace) }
+
+ let!(:wiki_page1) do
+ create(:wiki_page, wiki: project.wiki, attrs: { title: '3 home', content: '3' })
+ end
+ let!(:wiki_page2) do
+ create(:wiki_page, wiki: project.wiki, attrs: { title: '1 home', content: '1' })
+ end
+ let!(:wiki_page3) do
+ create(:wiki_page, wiki: project.wiki, attrs: { title: '2 home', content: '2' })
+ end
+
+ let(:pages) do
+ page.find('.wiki-pages-list').all('li').map { |li| li.find('a') }
+ end
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ visit(project_wikis_pages_path(project))
+ end
+
+ context 'ordered by title' do
+ let(:pages_ordered_by_title) { [wiki_page2, wiki_page3, wiki_page1] }
+
+ context 'asc' do
+ it 'pages are displayed in direct order' do
+ pages.each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_title[index].title)
+ end
+ end
+ end
+
+ context 'desc' do
+ before do
+ page.within('.wiki-sort-dropdown') do
+ page.find('.qa-reverse-sort').click
+ end
+ end
+
+ it 'pages are displayed in reversed order' do
+ pages.reverse_each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_title[index].title)
+ end
+ end
+ end
+ end
+
+ context 'ordered by created_at' do
+ let(:pages_ordered_by_created_at) { [wiki_page1, wiki_page2, wiki_page3] }
+
+ before do
+ page.within('.wiki-sort-dropdown') do
+ click_button('Title')
+ click_link('Created date')
+ end
+ end
+
+ context 'asc' do
+ it 'pages are displayed in direct order' do
+ pages.each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
+ end
+ end
+ end
+
+ context 'desc' do
+ before do
+ page.within('.wiki-sort-dropdown') do
+ page.find('.qa-reverse-sort').click
+ end
+ end
+
+ it 'pages are displayed in reversed order' do
+ pages.reverse_each.with_index do |page_title, index|
+ expect(page_title.text).to eq(pages_ordered_by_created_at[index].title)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index dbf0d427976..b5112758475 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Project' do
@@ -5,7 +7,7 @@ describe 'Project' do
include MobileHelpers
before do
- stub_feature_flags(approval_rules: false)
+ stub_feature_flags(vue_file_list: false)
end
describe 'creating from template' do
@@ -373,6 +375,21 @@ describe 'Project' do
end
end
+ describe 'edit' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:path) { edit_project_path(project) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ visit path
+ end
+
+ it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' },
+ { form: '.qa-merge-request-settings', input: '#project_printing_merge_request_link_enabled' }]
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 0aff916ec83..0dbff5a2701 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Branches', :js do
+ include ProtectedBranchHelpers
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
@@ -150,27 +152,11 @@ describe 'Protected Branches', :js do
end
describe "access control" do
- include_examples "protected branches > access control > CE"
- end
- end
-
- def set_protected_branch_name(branch_name)
- find(".js-protected-branch-select").click
- find(".dropdown-input-field").set(branch_name)
- click_on("Create wildcard #{branch_name}")
- end
-
- def set_defaults
- find(".js-allowed-to-merge").click
- within('.qa-allowed-to-merge-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
- end
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
- find(".js-allowed-to-push").click
- within('.qa-allowed-to-push-dropdown') do
- expect(first("li")).to have_content("Roles")
- find(:link, 'No one').click
+ include_examples "protected branches > access control > CE"
end
end
end
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index c8e92cd1c07..652542b1719 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Protected Tags', :js do
+ include ProtectedTagHelpers
+
let(:user) { create(:user, :admin) }
let(:project) { create(:project, :repository) }
@@ -8,13 +10,6 @@ describe 'Protected Tags', :js do
sign_in(user)
end
- def set_protected_tag_name(tag_name)
- find(".js-protected-tag-select").click
- find(".dropdown-input-field").set(tag_name)
- click_on("Create wildcard #{tag_name}")
- find('.protected-tags-dropdown .dropdown-menu', visible: false)
- end
-
describe "explicit protected tags" do
it "allows creating explicit protected tags" do
visit project_protected_tags_path(project)
@@ -92,6 +87,10 @@ describe 'Protected Tags', :js do
end
describe "access control" do
+ before do
+ stub_licensed_features(protected_refs_for_users: false)
+ end
+
include_examples "protected tags > access control > CE"
end
end
diff --git a/spec/features/raven_js_spec.rb b/spec/features/raven_js_spec.rb
index b0923b451ee..9a049764dec 100644
--- a/spec/features/raven_js_spec.rb
+++ b/spec/features/raven_js_spec.rb
@@ -3,13 +3,13 @@ require 'spec_helper'
describe 'RavenJS' do
let(:raven_path) { '/raven.chunk.js' }
- it 'should not load raven if sentry is disabled' do
+ it 'does not load raven if sentry is disabled' do
visit new_user_session_path
expect(has_requested_raven).to eq(false)
end
- it 'should load raven if sentry is enabled' do
+ it 'loads raven if sentry is enabled' do
stub_application_setting(clientside_sentry_dsn: 'https://key@domain.com/id', clientside_sentry_enabled: true)
visit new_user_session_path
diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb
new file mode 100644
index 00000000000..3725143291d
--- /dev/null
+++ b/spec/features/search/user_searches_for_users_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+describe 'User searches for users' do
+ context 'when on the dashboard' do
+ it 'finds the user' do
+ create(:user, username: 'gob_bluth', name: 'Gob Bluth')
+
+ sign_in(create(:user))
+
+ visit dashboard_projects_path
+
+ fill_in 'search', with: 'gob'
+ click_button 'Go'
+
+ expect(page).to have_content('Users 1')
+
+ click_on('Users 1')
+
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
+ end
+ end
+
+ context 'when on the project page' do
+ it 'finds the user belonging to the project' do
+ project = create(:project)
+
+ user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth')
+ create(:project_member, :developer, user: user1, project: project)
+
+ user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth')
+ create(:project_member, :developer, user: user2, project: project)
+
+ create(:user, username: 'gob_2018', name: 'George Oscar Bluth')
+
+ sign_in(user1)
+
+ visit projects_path(project)
+
+ fill_in 'search', with: 'gob'
+ click_button 'Go'
+
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
+
+ expect(page).not_to have_content('Michael Bluth')
+ expect(page).not_to have_content('@michael_bluth')
+
+ expect(page).not_to have_content('George Oscar Bluth')
+ expect(page).not_to have_content('@gob_2018')
+ end
+ end
+
+ context 'when on the group page' do
+ it 'finds the user belonging to the group' do
+ group = create(:group)
+
+ user1 = create(:user, username: 'gob_bluth', name: 'Gob Bluth')
+ create(:group_member, :developer, user: user1, group: group)
+
+ user2 = create(:user, username: 'michael_bluth', name: 'Michael Bluth')
+ create(:group_member, :developer, user: user2, group: group)
+
+ create(:user, username: 'gob_2018', name: 'George Oscar Bluth')
+
+ sign_in(user1)
+
+ visit group_path(group)
+
+ fill_in 'search', with: 'gob'
+ click_button 'Go'
+
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
+
+ expect(page).not_to have_content('Michael Bluth')
+ expect(page).not_to have_content('@michael_bluth')
+
+ expect(page).not_to have_content('George Oscar Bluth')
+ expect(page).not_to have_content('@gob_2018')
+ end
+ end
+end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 7225ca65492..6d4facd0649 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -14,22 +14,36 @@ describe 'User searches for wiki pages', :js do
include_examples 'top right search form'
- it 'finds a page' do
- find('.js-search-project-dropdown').click
+ shared_examples 'search wiki blobs' do
+ it 'finds a page' do
+ find('.js-search-project-dropdown').click
- page.within('.project-filter') do
- click_link(project.full_name)
- end
+ page.within('.project-filter') do
+ click_link(project.full_name)
+ end
+
+ fill_in('dashboard_search', with: 'content')
+ find('.btn-search').click
+
+ page.within('.search-filter') do
+ click_link('Wiki')
+ end
- fill_in('dashboard_search', with: 'content')
- find('.btn-search').click
+ page.within('.results') do
+ expect(find(:css, '.search-results')).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
+ end
+ end
+ end
- page.within('.search-filter') do
- click_link('Wiki')
+ context 'when searching by content' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'content' }
end
+ end
- page.within('.results') do
- expect(find(:css, '.search-results')).to have_link(wiki_page.title)
+ context 'when searching by title' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'test_wiki' }
end
end
end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 444de26733f..1cc47cd6bd1 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -36,7 +36,7 @@ describe 'User uses header search field' do
end
context 'when clicking merge requests' do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
find('.search-input-container .dropdown-menu').click_link('Merge requests assigned to me')
@@ -100,7 +100,7 @@ describe 'User uses header search field' do
end
context 'when clicking merge requests' do
- let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, author: user, assignees: [user]) }
it 'shows assigned merge requests' do
find('.dropdown-menu').click_link('Merge requests assigned to me')
diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb
index 4705cd12d23..a776169a8e5 100644
--- a/spec/features/security/group/private_access_spec.rb
+++ b/spec/features/security/group/private_access_spec.rb
@@ -93,4 +93,25 @@ describe 'Private Group access' do
it { is_expected.to be_denied_for(:visitor) }
it { is_expected.to be_denied_for(:external) }
end
+
+ describe 'GET /groups/:path for shared projects' do
+ let(:project) { create(:project, :public) }
+
+ before do
+ create(:project_group_link, project: project, group: group)
+ end
+
+ subject { group_path(group) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(group) }
+ it { is_expected.to be_allowed_for(:maintainer).of(group) }
+ it { is_expected.to be_allowed_for(:developer).of(group) }
+ it { is_expected.to be_allowed_for(:reporter).of(group) }
+ it { is_expected.to be_allowed_for(:guest).of(group) }
+ it { is_expected.to be_denied_for(project_guest) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
end
diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb
index a198e65046f..044a47567be 100644
--- a/spec/features/security/profile_access_spec.rb
+++ b/spec/features/security/profile_access_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe "Profile access" do
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
index eeacaf5f72a..78e0a43ce6d 100644
--- a/spec/features/snippets/notes_on_personal_snippets_spec.rb
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -70,7 +70,7 @@ describe 'Comments on personal snippets', :js do
fill_in 'note[note]', with: 'This is **awesome**!'
find('.js-md-preview-button').click
- page.within('.new-note .md-preview') do
+ page.within('.new-note .md-preview-holder') do
expect(page).to have_content('This is awesome!')
expect(page).to have_selector('strong')
end
@@ -83,7 +83,7 @@ describe 'Comments on personal snippets', :js do
expect(find('div#notes')).to have_content('This is awesome!')
end
- it 'should not have autocomplete' do
+ it 'does not have autocomplete' do
wait_for_requests
find('#note_note').native.send_keys('')
diff --git a/spec/features/snippets/user_creates_snippet_spec.rb b/spec/features/snippets/user_creates_snippet_spec.rb
index 879c46d7c4e..1c97d5ec5b4 100644
--- a/spec/features/snippets/user_creates_snippet_spec.rb
+++ b/spec/features/snippets/user_creates_snippet_spec.rb
@@ -37,7 +37,7 @@ describe 'User creates snippet', :js do
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
find('.js-md-preview-button').click
- page.within('#new_personal_snippet .md-preview') do
+ page.within('#new_personal_snippet .md-preview-holder') do
expect(page).to have_content('My Snippet')
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb
index 8d567e925ef..bdbbe645779 100644
--- a/spec/features/tags/master_deletes_tag_spec.rb
+++ b/spec/features/tags/master_deletes_tag_spec.rb
@@ -37,7 +37,7 @@ describe 'Maintainer deletes tag' do
context 'when pre-receive hook fails', :js do
before do
allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag)
- .and_raise(Gitlab::Git::PreReceiveError, 'Do not delete tags')
+ .and_raise(Gitlab::Git::PreReceiveError, 'GitLab: Do not delete tags')
end
it 'shows the error message' do
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 6fe840dccf6..33d9c10f5e8 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -79,7 +79,7 @@ describe 'Task Lists' do
visit_issue(project, issue)
wait_for_requests
- expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector(".md .task-list .task-list-item .task-list-item-checkbox")
expect(page).to have_selector('a.btn-close')
end
@@ -87,14 +87,14 @@ describe 'Task Lists' do
visit_issue(project, issue)
wait_for_requests
- expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector(".md .task-list .task-list-item .task-list-item-checkbox")
logout(:user)
login_as(user2)
visit current_path
wait_for_requests
- expect(page).to have_selector(".wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector(".md .task-list .task-list-item .task-list-item-checkbox")
end
it 'provides a summary on Issues#index' do
@@ -231,7 +231,7 @@ describe 'Task Lists' do
container = '.detail-page-description .description.js-task-list-container'
expect(page).to have_selector(container)
- expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector("#{container} .md .task-list .task-list-item .task-list-item-checkbox")
expect(page).to have_selector("#{container} .js-task-list-field")
expect(page).to have_selector('form.js-issuable-update')
expect(page).to have_selector('a.btn-close')
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index ae9b65d1a39..ea02f36d9d0 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -246,26 +246,6 @@ describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
end
end
end
-
- describe "when two-factor authentication is disabled" do
- let(:user) { create(:user) }
-
- before do
- user = gitlab_sign_in(:user)
- user.update_attribute(:otp_required_for_login, true)
- visit profile_account_path
- manage_two_factor_authentication
- expect(page).to have_content("Your U2F device needs to be set up.")
- register_u2f_device
- end
-
- it "deletes u2f registrations" do
- visit profile_two_factor_auth_path
- expect do
- accept_confirm { click_on "Disable" }
- end.to change { U2fRegistration.count }.by(-1)
- end
- end
end
describe 'fallback code authentication' do
diff --git a/spec/features/user_opens_link_to_comment.rb b/spec/features/user_opens_link_to_comment.rb
new file mode 100644
index 00000000000..f1e07e55799
--- /dev/null
+++ b/spec/features/user_opens_link_to_comment.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'User opens link to comment', :js do
+ let(:project) { create(:project, :public) }
+ let(:note) { create(:note_on_issue, project: project) }
+
+ context 'authenticated user' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ it 'switches to all activity and does not show error message' do
+ create(:user_preference, user: user, issue_notes_filter: UserPreference::NOTES_FILTERS[:only_activity])
+
+ visit Gitlab::UrlBuilder.build(note)
+
+ expect(page.find('#discussion-filter-dropdown')).to have_content('Show all activity')
+ expect(page).not_to have_content('Something went wrong while fetching comments')
+ end
+ end
+
+ context 'anonymous user' do
+ it 'does not show error message' do
+ visit Gitlab::UrlBuilder.build(note)
+
+ expect(page).not_to have_content('Something went wrong while fetching comments')
+ end
+ end
+end
diff --git a/spec/features/user_sees_revert_modal_spec.rb b/spec/features/user_sees_revert_modal_spec.rb
index 3b48ea4786d..d2cdade88d1 100644
--- a/spec/features/user_sees_revert_modal_spec.rb
+++ b/spec/features/user_sees_revert_modal_spec.rb
@@ -17,12 +17,14 @@ describe 'Merge request > User sees revert modal', :js do
end
it 'shows the revert modal' do
- expect(page).to have_content('Revert this merge request')
+ page.within('.modal-header') do
+ expect(page).to have_content 'Revert this merge request'
+ end
end
it 'closes the revert modal with escape keypress' do
find('#modal-revert-commit').send_keys(:escape)
- expect(page).not_to have_content('Revert this merge request')
+ expect(page).not_to have_selector('#modal-revert-commit', visible: true)
end
end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index ad856bd062e..efba303033b 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -137,7 +137,7 @@ describe 'Login' do
enter_code(user.current_otp)
- expect(page).not_to have_content('You are already signed in.')
+ expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
context 'using one-time code' do
@@ -317,7 +317,17 @@ describe 'Login' do
gitlab_sign_in(user)
expect(current_path).to eq root_path
- expect(page).not_to have_content('You are already signed in.')
+ expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
+ end
+
+ it 'does not show already signed in message when opening sign in page after login' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+
+ gitlab_sign_in(user)
+ visit new_user_session_path
+
+ expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
end
@@ -434,16 +444,22 @@ describe 'Login' do
context 'within the grace period' do
it 'redirects to two-factor configuration page' do
- expect(authentication_metrics)
- .to increment(:user_authenticated_counter)
-
- gitlab_sign_in(user)
-
- expect(current_path).to eq profile_two_factor_auth_path
- expect(page).to have_content(
- 'The group settings for Group 1 and Group 2 require you to enable ' \
- 'Two-Factor Authentication for your account. You need to do this ' \
- 'before ')
+ Timecop.freeze do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+
+ gitlab_sign_in(user)
+
+ expect(current_path).to eq profile_two_factor_auth_path
+ expect(page).to have_content(
+ 'The group settings for Group 1 and Group 2 require you to enable '\
+ 'Two-Factor Authentication for your account. '\
+ 'You can leave Group 1 and leave Group 2. '\
+ 'You need to do this '\
+ 'before '\
+ "#{(Time.zone.now + 2.days).strftime("%a, %d %b %Y %H:%M:%S %z")}"
+ )
+ end
end
it 'allows skipping two-factor configuration', :js do
@@ -500,7 +516,8 @@ describe 'Login' do
expect(current_path).to eq profile_two_factor_auth_path
expect(page).to have_content(
'The group settings for Group 1 and Group 2 require you to enable ' \
- 'Two-Factor Authentication for your account.'
+ 'Two-Factor Authentication for your account. '\
+ 'You can leave Group 1 and leave Group 2.'
)
end
end
@@ -572,7 +589,7 @@ describe 'Login' do
click_button 'Accept terms'
expect(current_path).to eq(root_path)
- expect(page).not_to have_content('You are already signed in.')
+ expect(page).not_to have_content(I18n.t('devise.failure.already_authenticated'))
end
it 'does not ask for terms when the user already accepted them' do
diff --git a/spec/features/users/overview_spec.rb b/spec/features/users/overview_spec.rb
index 3db9ae7a951..bfa85696e19 100644
--- a/spec/features/users/overview_spec.rb
+++ b/spec/features/users/overview_spec.rb
@@ -93,7 +93,7 @@ describe 'Overview tab on a user profile', :js do
describe 'user has no personal projects' do
include_context 'visit overview tab'
- it 'it shows an empty project list with an info message' do
+ it 'shows an empty project list with an info message' do
page.within('.projects-block') do
expect(page).to have_selector('.loading', visible: false)
expect(page).to have_content('You haven\'t created any personal projects.')
@@ -113,7 +113,7 @@ describe 'Overview tab on a user profile', :js do
include_context 'visit overview tab'
- it 'it shows one entry in the list of projects' do
+ it 'shows one entry in the list of projects' do
page.within('.projects-block') do
expect(page).to have_selector('.project-row', count: 1)
end
@@ -139,7 +139,7 @@ describe 'Overview tab on a user profile', :js do
include_context 'visit overview tab'
- it 'it shows max. ten entries in the list of projects' do
+ it 'shows max. ten entries in the list of projects' do
page.within('.projects-block') do
expect(page).to have_selector('.project-row', count: 10)
end
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index 86379164cf0..351750c0179 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'User page' do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
context 'with public profile' do
@@ -86,4 +88,24 @@ describe 'User page' do
end
end
end
+
+ context 'most recent activity' do
+ it 'shows the most recent activity' do
+ visit(user_path(user))
+
+ expect(page).to have_content('Most Recent Activity')
+ end
+
+ context 'when external authorization is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'hides the most recent activity' do
+ visit(user_path(user))
+
+ expect(page).not_to have_content('Most Recent Activity')
+ end
+ end
+ end
end
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index 957c3cfc583..1a9caf0ffbb 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -25,6 +25,13 @@ describe 'Signup' do
expect(find('.username')).not_to have_css '.gl-field-error-outline'
end
+ it 'does not show an error border if the username length is not longer than 255 characters' do
+ fill_in 'new_user_username', with: 'u' * 255
+ wait_for_requests
+
+ expect(find('.username')).not_to have_css '.gl-field-error-outline'
+ end
+
it 'shows an error border if the username already exists' do
existing_user = create(:user)
@@ -41,6 +48,20 @@ describe 'Signup' do
expect(find('.username')).to have_css '.gl-field-error-outline'
end
+ it 'shows an error border if the username is longer than 255 characters' do
+ fill_in 'new_user_username', with: 'u' * 256
+ wait_for_requests
+
+ expect(find('.username')).to have_css '.gl-field-error-outline'
+ end
+
+ it 'shows an error message if the username is longer than 255 characters' do
+ fill_in 'new_user_username', with: 'u' * 256
+ wait_for_requests
+
+ expect(page).to have_content("Username is too long (maximum is 255 characters).")
+ end
+
it 'shows an error message on submit if the username contains special characters' do
fill_in 'new_user_username', with: 'new$user!username'
wait_for_requests
@@ -67,14 +88,35 @@ describe 'Signup' do
before do
visit root_path
click_link 'Register'
- simulate_input('#new_user_name', 'Ehsan 🦋')
+ end
+
+ it 'does not show an error border if the user\'s fullname length is not longer than 128 characters' do
+ fill_in 'new_user_name', with: 'u' * 128
+
+ expect(find('.name')).not_to have_css '.gl-field-error-outline'
end
it 'shows an error border if the user\'s fullname contains an emoji' do
+ simulate_input('#new_user_name', 'Ehsan 🦋')
+
+ expect(find('.name')).to have_css '.gl-field-error-outline'
+ end
+
+ it 'shows an error border if the user\'s fullname is longer than 128 characters' do
+ fill_in 'new_user_name', with: 'n' * 129
+
expect(find('.name')).to have_css '.gl-field-error-outline'
end
+ it 'shows an error message if the user\'s fullname is longer than 128 characters' do
+ fill_in 'new_user_name', with: 'n' * 129
+
+ expect(page).to have_content("Name is too long (maximum is 128 characters).")
+ end
+
it 'shows an error message if the username contains emojis' do
+ simulate_input('#new_user_name', 'Ehsan 🦋')
+
expect(page).to have_content("Invalid input, please avoid emojis")
end
end
diff --git a/spec/finders/admin/runners_finder_spec.rb b/spec/finders/admin/runners_finder_spec.rb
index 0b2325cc7ca..94ccb398801 100644
--- a/spec/finders/admin/runners_finder_spec.rb
+++ b/spec/finders/admin/runners_finder_spec.rb
@@ -37,6 +37,14 @@ describe Admin::RunnersFinder do
end
end
+ context 'filter by tag_name' do
+ it 'calls the corresponding scope on Ci::Runner' do
+ expect(Ci::Runner).to receive(:tagged_with).with(%w[tag1 tag2]).and_call_original
+
+ described_class.new(params: { tag_name: %w[tag1 tag2] }).execute
+ end
+ end
+
context 'sort' do
context 'without sort param' do
it 'sorts by created_at' do
diff --git a/spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb b/spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb
new file mode 100644
index 00000000000..79d2f9cdb45
--- /dev/null
+++ b/spec/finders/autocomplete/acts_as_taggable_on/tags_finder_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Autocomplete::ActsAsTaggableOn::TagsFinder do
+ describe '#execute' do
+ context 'with empty params' do
+ it 'returns all tags' do
+ tag1 = ActsAsTaggableOn::Tag.create!(name: 'tag1')
+ tag2 = ActsAsTaggableOn::Tag.create!(name: 'tag2')
+
+ tags = described_class.new(params: {}).execute
+
+ expect(tags).to match_array [tag1, tag2]
+ end
+ end
+
+ context 'filter by search' do
+ context 'with an empty search term' do
+ it 'returns an empty collection' do
+ ActsAsTaggableOn::Tag.create!(name: 'tag1')
+ ActsAsTaggableOn::Tag.create!(name: 'tag2')
+
+ tags = described_class.new(params: { search: '' }).execute
+
+ expect(tags).to be_empty
+ end
+ end
+
+ context 'with a search containing 2 characters' do
+ it 'returns the tag that strictly matches the search term' do
+ tag1 = ActsAsTaggableOn::Tag.create!(name: 't1')
+ ActsAsTaggableOn::Tag.create!(name: 't11')
+
+ tags = described_class.new(params: { search: 't1' }).execute
+
+ expect(tags).to match_array [tag1]
+ end
+ end
+
+ context 'with a search containing 3 characters' do
+ it 'returns the tag that partially matches the search term' do
+ tag1 = ActsAsTaggableOn::Tag.create!(name: 'tag1')
+ tag2 = ActsAsTaggableOn::Tag.create!(name: 'tag11')
+
+ tags = described_class.new(params: { search: 'ag1' }).execute
+
+ expect(tags).to match_array [tag1, tag2]
+ end
+ end
+ end
+
+ context 'limit' do
+ it 'limits the result set by the limit constant' do
+ stub_const("#{described_class}::LIMIT", 1)
+
+ ActsAsTaggableOn::Tag.create!(name: 'tag1')
+ ActsAsTaggableOn::Tag.create!(name: 'tag2')
+
+ tags = described_class.new(params: { search: 'tag' }).execute
+
+ expect(tags.count).to eq 1
+ end
+ end
+ end
+end
diff --git a/spec/finders/autocomplete/users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb
index abd0d6b5185..bcde115b1a6 100644
--- a/spec/finders/autocomplete/users_finder_spec.rb
+++ b/spec/finders/autocomplete/users_finder_spec.rb
@@ -26,9 +26,17 @@ describe Autocomplete::UsersFinder do
it { is_expected.to match_array([project.owner]) }
context 'when author_id passed' do
- let(:params) { { author_id: user2.id } }
+ context 'and author is active' do
+ let(:params) { { author_id: user1.id } }
- it { is_expected.to match_array([project.owner, user2]) }
+ it { is_expected.to match_array([project.owner, user1]) }
+ end
+
+ context 'and author is blocked' do
+ let(:params) { { author_id: user2.id } }
+
+ it { is_expected.to match_array([project.owner]) }
+ end
end
end
@@ -104,9 +112,9 @@ describe Autocomplete::UsersFinder do
end
context 'when filtered by author_id' do
- let(:params) { { author_id: user2.id } }
+ let(:params) { { author_id: user1.id } }
- it { is_expected.to match_array([user2, user1, external_user, omniauth_user, current_user]) }
+ it { is_expected.to match_array([user1, external_user, omniauth_user, current_user]) }
end
end
end
diff --git a/spec/finders/cluster_ancestors_finder_spec.rb b/spec/finders/cluster_ancestors_finder_spec.rb
index 332086c42e2..750042b6b54 100644
--- a/spec/finders/cluster_ancestors_finder_spec.rb
+++ b/spec/finders/cluster_ancestors_finder_spec.rb
@@ -8,11 +8,15 @@ describe ClusterAncestorsFinder, '#execute' do
let(:user) { create(:user) }
let!(:project_cluster) do
- create(:cluster, :provided_by_user, cluster_type: :project_type, projects: [project])
+ create(:cluster, :provided_by_user, :project, projects: [project])
end
let!(:group_cluster) do
- create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group])
+ create(:cluster, :provided_by_user, :group, groups: [group])
+ end
+
+ let!(:instance_cluster) do
+ create(:cluster, :provided_by_user, :instance)
end
subject { described_class.new(clusterable, user).execute }
@@ -25,7 +29,7 @@ describe ClusterAncestorsFinder, '#execute' do
end
it 'returns the project clusters followed by group clusters' do
- is_expected.to eq([project_cluster, group_cluster])
+ is_expected.to eq([project_cluster, group_cluster, instance_cluster])
end
context 'nested groups', :nested_groups do
@@ -33,11 +37,11 @@ describe ClusterAncestorsFinder, '#execute' do
let(:parent_group) { create(:group) }
let!(:parent_group_cluster) do
- create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group])
+ create(:cluster, :provided_by_user, :group, groups: [parent_group])
end
it 'returns the project clusters followed by group clusters ordered ascending the hierarchy' do
- is_expected.to eq([project_cluster, group_cluster, parent_group_cluster])
+ is_expected.to eq([project_cluster, group_cluster, parent_group_cluster, instance_cluster])
end
end
end
@@ -58,7 +62,7 @@ describe ClusterAncestorsFinder, '#execute' do
end
it 'returns the list of group clusters' do
- is_expected.to eq([group_cluster])
+ is_expected.to eq([group_cluster, instance_cluster])
end
context 'nested groups', :nested_groups do
@@ -66,12 +70,21 @@ describe ClusterAncestorsFinder, '#execute' do
let(:parent_group) { create(:group) }
let!(:parent_group_cluster) do
- create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [parent_group])
+ create(:cluster, :provided_by_user, :group, groups: [parent_group])
end
it 'returns the list of group clusters ordered ascending the hierarchy' do
- is_expected.to eq([group_cluster, parent_group_cluster])
+ is_expected.to eq([group_cluster, parent_group_cluster, instance_cluster])
end
end
end
+
+ context 'for an instance' do
+ let(:clusterable) { Clusters::Instance.new }
+ let(:user) { create(:admin) }
+
+ it 'returns the list of instance clusters' do
+ is_expected.to eq([instance_cluster])
+ end
+ end
end
diff --git a/spec/finders/clusters/knative_services_finder_spec.rb b/spec/finders/clusters/knative_services_finder_spec.rb
new file mode 100644
index 00000000000..b731c2bd6bf
--- /dev/null
+++ b/spec/finders/clusters/knative_services_finder_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::KnativeServicesFinder do
+ include KubernetesHelpers
+ include ReactiveCachingHelpers
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:service) { cluster.platform_kubernetes }
+ let(:project) { cluster.cluster_project.project }
+ let(:namespace) do
+ create(:cluster_kubernetes_namespace,
+ cluster: cluster,
+ cluster_project: cluster.cluster_project,
+ project: project)
+ end
+
+ before do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ stub_kubeclient_service_pods(
+ kube_response(
+ kube_knative_pods_body(
+ project.name, namespace.namespace
+ )
+ ),
+ namespace: namespace.namespace
+ )
+ end
+
+ shared_examples 'a cached data' do
+ it 'has an unintialized cache' do
+ is_expected.to be_blank
+ end
+
+ context 'when using synchronous reactive cache' do
+ before do
+ synchronous_reactive_cache(cluster.knative_services_finder(project))
+ end
+
+ context 'when there are functions for cluster namespace' do
+ it { is_expected.not_to be_blank }
+ end
+
+ context 'when there are no functions for cluster namespace' do
+ before do
+ stub_kubeclient_knative_services(
+ namespace: namespace.namespace,
+ response: kube_response({ "kind" => "ServiceList", "items" => [] })
+ )
+ stub_kubeclient_service_pods(
+ kube_response({ "kind" => "PodList", "items" => [] }),
+ namespace: namespace.namespace
+ )
+ end
+
+ it { is_expected.to be_blank }
+ end
+ end
+ end
+
+ describe '#service_pod_details' do
+ subject { cluster.knative_services_finder(project).service_pod_details(project.name) }
+
+ it_behaves_like 'a cached data'
+ end
+
+ describe '#services' do
+ subject { cluster.knative_services_finder(project).services }
+
+ it_behaves_like 'a cached data'
+ end
+
+ describe '#knative_detected' do
+ subject { cluster.knative_services_finder(project).knative_detected }
+ before do
+ synchronous_reactive_cache(cluster.knative_services_finder(project))
+ end
+
+ context 'when knative is installed' do
+ before do
+ stub_kubeclient_discover(service.api_url)
+ end
+
+ it { is_expected.to be_truthy }
+ it "discovers knative installation" do
+ expect { subject }
+ .to change { cluster.kubeclient.knative_client.discovered }
+ .from(false)
+ .to(true)
+ end
+ end
+
+ context 'when knative is not installed' do
+ before do
+ stub_kubeclient_discover_knative_not_found(service.api_url)
+ end
+
+ it { is_expected.to be_falsy }
+ it "does not discover knative installation" do
+ expect { subject }.not_to change { cluster.kubeclient.knative_client.discovered }
+ end
+ end
+ end
+end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index d6d95906f5e..f8fcc2d0e40 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -1,26 +1,7 @@
require 'spec_helper'
describe GroupProjectsFinder do
- let(:group) { create(:group) }
- let(:subgroup) { create(:group, parent: group) }
- let(:current_user) { create(:user) }
- let(:options) { {} }
-
- let(:finder) { described_class.new(group: group, current_user: current_user, options: options) }
-
- let!(:public_project) { create(:project, :public, group: group, path: '1') }
- let!(:private_project) { create(:project, :private, group: group, path: '2') }
- let!(:shared_project_1) { create(:project, :public, path: '3') }
- let!(:shared_project_2) { create(:project, :private, path: '4') }
- let!(:shared_project_3) { create(:project, :internal, path: '5') }
- let!(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) }
- let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) }
-
- before do
- shared_project_1.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
- shared_project_2.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
- shared_project_3.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
- end
+ include_context 'GroupProjectsFinder context'
subject { finder.execute }
@@ -144,6 +125,24 @@ describe GroupProjectsFinder do
end
end
+ describe 'with an admin current user' do
+ let(:current_user) { create(:admin) }
+
+ context "only shared" do
+ let(:options) { { only_shared: true } }
+ it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1]) }
+ end
+
+ context "only owned" do
+ let(:options) { { only_owned: true } }
+ it { is_expected.to eq([private_project, public_project]) }
+ end
+
+ context "all" do
+ it { is_expected.to eq([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
+ end
+ end
+
describe "no user" do
context "only shared" do
let(:options) { { only_shared: true } }
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 47e2548c3d6..89fdaceaa9f 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -1,45 +1,10 @@
require 'spec_helper'
describe IssuesFinder do
- set(:user) { create(:user) }
- set(:user2) { create(:user) }
- set(:group) { create(:group) }
- set(:subgroup) { create(:group, parent: group) }
- set(:project1) { create(:project, group: group) }
- set(:project2) { create(:project) }
- set(:project3) { create(:project, group: subgroup) }
- set(:milestone) { create(:milestone, project: project1) }
- set(:label) { create(:label, project: project2) }
- set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) }
- set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) }
- set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) }
- set(:issue4) { create(:issue, project: project3) }
- set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
- set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
- set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
+ include_context 'IssuesFinder context'
describe '#execute' do
- let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
- let!(:label_link) { create(:label_link, label: label, target: issue2) }
- let(:search_user) { user }
- let(:params) { {} }
- let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
-
- before(:context) do
- project1.add_maintainer(user)
- project2.add_developer(user)
- project2.add_developer(user2)
- project3.add_developer(user)
-
- issue1
- issue2
- issue3
- issue4
-
- award_emoji1
- award_emoji2
- award_emoji3
- end
+ include_context 'IssuesFinder#execute context'
context 'scope: all' do
let(:scope) { 'all' }
@@ -48,45 +13,32 @@ describe IssuesFinder do
expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
- context 'filtering by assignee ID' do
- let(:params) { { assignee_id: user.id } }
-
- it 'returns issues assigned to that user' do
- expect(issues).to contain_exactly(issue1, issue2)
- end
- end
-
- context 'filtering by no assignee' do
- let(:params) { { assignee_id: 'None' } }
+ context 'assignee filtering' do
+ let(:issuables) { issues }
- it 'returns issues not assigned to any assignee' do
- expect(issues).to contain_exactly(issue4)
+ it_behaves_like 'assignee ID filter' do
+ let(:params) { { assignee_id: user.id } }
+ let(:expected_issuables) { [issue1, issue2] }
end
- it 'returns issues not assigned to any assignee' do
- params[:assignee_id] = 0
-
- expect(issues).to contain_exactly(issue4)
- end
-
- it 'returns issues not assigned to any assignee' do
- params[:assignee_id] = 'none'
+ it_behaves_like 'assignee username filter' do
+ before do
+ project2.add_developer(user3)
+ issue3.assignees = [user2, user3]
+ end
- expect(issues).to contain_exactly(issue4)
+ set(:user3) { create(:user) }
+ let(:params) { { assignee_username: [user2.username, user3.username] } }
+ let(:expected_issuables) { [issue3] }
end
- end
-
- context 'filtering by any assignee' do
- let(:params) { { assignee_id: 'Any' } }
- it 'returns issues assigned to any assignee' do
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ it_behaves_like 'no assignee filter' do
+ set(:user3) { create(:user) }
+ let(:expected_issuables) { [issue4] }
end
- it 'returns issues assigned to any assignee' do
- params[:assignee_id] = 'any'
-
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ it_behaves_like 'any assignee filter' do
+ let(:expected_issuables) { [issue1, issue2, issue3] }
end
end
@@ -220,6 +172,7 @@ describe IssuesFinder do
let(:yesterday) { Date.today - 1.day }
let(:tomorrow) { Date.today + 1.day }
let(:two_days_ago) { Date.today - 2.days }
+ let(:three_days_ago) { Date.today - 3.days }
let(:milestones) do
[
@@ -227,6 +180,8 @@ describe IssuesFinder do
create(:milestone, project: project_started_1_and_2, title: '1.0', start_date: two_days_ago),
create(:milestone, project: project_started_1_and_2, title: '2.0', start_date: yesterday),
create(:milestone, project: project_started_1_and_2, title: '3.0', start_date: tomorrow),
+ create(:milestone, :closed, project: project_started_1_and_2, title: '4.0', start_date: three_days_ago),
+ create(:milestone, :closed, project: project_started_8, title: '6.0', start_date: three_days_ago),
create(:milestone, project: project_started_8, title: '7.0'),
create(:milestone, project: project_started_8, title: '8.0', start_date: yesterday),
create(:milestone, project: project_started_8, title: '9.0', start_date: tomorrow)
@@ -576,6 +531,13 @@ describe IssuesFinder do
expect(issues.count).to eq 0
end
end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:issue, project: project) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
end
describe '#row_count', :request_store do
@@ -640,6 +602,16 @@ describe IssuesFinder do
expect(subject).to include(public_issue, confidential_issue)
end
end
+
+ context 'for an admin' do
+ let(:admin_user) { create(:user, :admin) }
+
+ subject { described_class.new(admin_user, params).with_confidentiality_access_check }
+
+ it 'returns all issues' do
+ expect(subject).to include(public_issue, confidential_issue)
+ end
+ end
end
context 'when searching within a specific project' do
@@ -669,9 +641,7 @@ describe IssuesFinder do
end
it 'filters by confidentiality' do
- expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
-
- subject
+ expect(subject.to_sql).to match("issues.confidential")
end
end
@@ -688,9 +658,7 @@ describe IssuesFinder do
end
it 'filters by confidentiality' do
- expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
-
- subject
+ expect(subject.to_sql).to match("issues.confidential")
end
end
@@ -707,62 +675,21 @@ describe IssuesFinder do
subject
end
end
- end
- end
-
- describe '#use_subquery_for_search?' do
- let(:finder) { described_class.new(nil, params) }
-
- before do
- allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
- stub_feature_flags(use_subquery_for_group_issues_search: true)
- end
-
- context 'when there is no search param' do
- let(:params) { { attempt_group_search_optimizations: true } }
-
- it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
- end
- end
-
- context 'when the database is not Postgres' do
- let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
-
- before do
- allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
- end
-
- it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
- end
- end
-
- context 'when the attempt_group_search_optimizations param is falsey' do
- let(:params) { { search: 'foo' } }
-
- it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
- end
- end
- context 'when the use_subquery_for_group_issues_search flag is disabled' do
- let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+ context 'for an admin' do
+ let(:admin_user) { create(:user, :admin) }
- before do
- stub_feature_flags(use_subquery_for_group_issues_search: false)
- end
+ subject { described_class.new(admin_user, params).with_confidentiality_access_check }
- it 'returns false' do
- expect(finder.use_subquery_for_search?).to be_falsey
- end
- end
+ it 'returns all issues' do
+ expect(subject).to include(public_issue, confidential_issue)
+ end
- context 'when all conditions are met' do
- let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+ it 'does not filter by confidentiality' do
+ expect(Issue).not_to receive(:where).with(a_string_matching('confidential'), anything)
- it 'returns true' do
- expect(finder.use_subquery_for_search?).to be_truthy
+ subject
+ end
end
end
end
@@ -772,8 +699,7 @@ describe IssuesFinder do
before do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
- stub_feature_flags(use_cte_for_group_issues_search: true)
- stub_feature_flags(use_subquery_for_group_issues_search: false)
+ stub_feature_flags(attempt_group_search_optimizations: true)
end
context 'when there is no search param' do
@@ -796,7 +722,7 @@ describe IssuesFinder do
end
end
- context 'when the attempt_group_search_optimizations param is falsey' do
+ context 'when the force_cte param is falsey' do
let(:params) { { search: 'foo' } }
it 'returns false' do
@@ -804,11 +730,11 @@ describe IssuesFinder do
end
end
- context 'when the use_cte_for_group_issues_search flag is disabled' do
+ context 'when the attempt_group_search_optimizations flag is disabled' do
let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
before do
- stub_feature_flags(use_cte_for_group_issues_search: false)
+ stub_feature_flags(attempt_group_search_optimizations: false)
end
it 'returns false' do
@@ -816,15 +742,27 @@ describe IssuesFinder do
end
end
- context 'when use_subquery_for_search? is true' do
- let(:params) { { search: 'foo', attempt_group_search_optimizations: true } }
+ context 'when attempt_group_search_optimizations is unset and attempt_project_search_optimizations is set' do
+ let(:params) { { search: 'foo', attempt_project_search_optimizations: true } }
- before do
- stub_feature_flags(use_subquery_for_group_issues_search: true)
+ context 'and the corresponding feature flag is disabled' do
+ before do
+ stub_feature_flags(attempt_project_search_optimizations: false)
+ end
+
+ it 'returns false' do
+ expect(finder.use_cte_for_search?).to be_falsey
+ end
end
- it 'returns false' do
- expect(finder.use_cte_for_search?).to be_falsey
+ context 'and the corresponding feature flag is enabled' do
+ before do
+ stub_feature_flags(attempt_project_search_optimizations: true)
+ end
+
+ it 'returns true' do
+ expect(finder.use_cte_for_search?).to be_truthy
+ end
end
end
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index 9abc52aa664..98b4933fef6 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -209,6 +209,12 @@ describe LabelsFinder do
expect(finder.execute).to eq [project_label_1]
end
+
+ it 'returns labels matching a single character' do
+ finder = described_class.new(user, search: '(')
+
+ expect(finder.execute).to eq [group_label_1]
+ end
end
context 'filter by subscription' do
@@ -220,5 +226,12 @@ describe LabelsFinder do
expect(finder.execute).to eq [project_label_1]
end
end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:label, project: project) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
end
end
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index db48f00cd74..83348457caa 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -1,13 +1,13 @@
require 'spec_helper'
describe MembersFinder, '#execute' do
- let(:group) { create(:group) }
- let(:nested_group) { create(:group, :access_requestable, parent: group) }
- let(:project) { create(:project, namespace: nested_group) }
- let(:user1) { create(:user) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:user4) { create(:user) }
+ set(:group) { create(:group) }
+ set(:nested_group) { create(:group, :access_requestable, parent: group) }
+ set(:project) { create(:project, namespace: nested_group) }
+ set(:user1) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:user3) { create(:user) }
+ set(:user4) { create(:user) }
it 'returns members for project and parent groups', :nested_groups do
nested_group.request_access(user1)
@@ -31,4 +31,34 @@ describe MembersFinder, '#execute' do
expect(result.to_a).to match_array([member1, member2, member3])
end
+
+ context 'when include_invited_groups_members == true', :nested_groups do
+ subject { described_class.new(project, user2).execute(include_invited_groups_members: true) }
+
+ set(:linked_group) { create(:group, :public, :access_requestable) }
+ set(:nested_linked_group) { create(:group, parent: linked_group) }
+ set(:linked_group_member) { linked_group.add_developer(user1) }
+ set(:nested_linked_group_member) { nested_linked_group.add_developer(user2) }
+
+ it 'includes all the invited_groups members including members inherited from ancestor groups', :nested_groups do
+ create(:project_group_link, project: project, group: nested_linked_group)
+
+ expect(subject).to contain_exactly(linked_group_member, nested_linked_group_member)
+ end
+
+ it 'includes all the invited_groups members' do
+ create(:project_group_link, project: project, group: linked_group)
+
+ expect(subject).to contain_exactly(linked_group_member)
+ end
+
+ it 'excludes group_members not visible to the user' do
+ create(:project_group_link, project: project, group: linked_group)
+ private_linked_group = create(:group, :private)
+ private_linked_group.add_developer(user3)
+ create(:project_group_link, project: project, group: private_linked_group)
+
+ expect(subject).to contain_exactly(linked_group_member)
+ end
+ end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 107da08a0a9..da5e9dab058 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -1,287 +1,416 @@
require 'spec_helper'
describe MergeRequestsFinder do
- include ProjectForksHelper
-
- # We need to explicitly permit Gitaly N+1s because of the specs that use
- # :request_store. Gitaly N+1 detection is only enabled when :request_store is,
- # but we don't care about potential N+1s when we're just creating several
- # projects in the setup phase.
- def create_project_without_n_plus_1(*args)
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- create(:project, :public, *args)
- end
- end
+ context "multiple projects with merge requests" do
+ include_context 'MergeRequestsFinder multiple projects with merge requests context'
- let(:user) { create :user }
- let(:user2) { create :user }
+ describe '#execute' do
+ it 'filters by scope' do
+ params = { scope: 'authored', state: 'opened' }
- let(:group) { create(:group) }
- let(:subgroup) { create(:group, parent: group) }
- let(:project1) { create_project_without_n_plus_1(group: group) }
- let(:project2) do
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- fork_project(project1, user)
- end
- end
- let(:project3) do
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- p = fork_project(project1, user)
- p.update!(archived: true)
- p
- end
- end
- let(:project4) { create_project_without_n_plus_1(group: subgroup) }
- let(:project5) { create_project_without_n_plus_1(group: subgroup) }
- let(:project6) { create_project_without_n_plus_1(group: subgroup) }
-
- let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
- let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
- let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
- let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') }
- let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') }
- let!(:merge_request6) { create(:merge_request, :simple, author: user, source_project: project5, target_project: project5, title: 'WIP: thing') }
- let!(:merge_request7) { create(:merge_request, :simple, author: user, source_project: project6, target_project: project6, title: 'wip thing') }
- let!(:merge_request8) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project1, title: '[wip] thing') }
- let!(:merge_request9) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project2, title: 'wip: thing') }
-
- before do
- project1.add_maintainer(user)
- project2.add_developer(user)
- project3.add_developer(user)
- project2.add_developer(user2)
- project4.add_developer(user)
- project5.add_developer(user)
- project6.add_developer(user)
- end
+ merge_requests = described_class.new(user, params).execute
- describe "#execute" do
- it 'filters by scope' do
- params = { scope: 'authored', state: 'opened' }
- merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(7)
- end
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request4, merge_request5)
+ end
- it 'filters by project' do
- params = { project_id: project1.id, scope: 'authored', state: 'opened' }
- merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(2)
- end
+ it 'filters by project' do
+ params = { project_id: project1.id, scope: 'authored', state: 'opened' }
- context 'filtering by group' do
- it 'includes all merge requests when user has access' do
- params = { group_id: group.id }
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request1)
+ end
+
+ it 'filters by commit sha' do
+ merge_requests = described_class.new(
+ user,
+ commit_sha: merge_request5.merge_request_diff.last_commit_sha
+ ).execute
+
+ expect(merge_requests).to contain_exactly(merge_request5)
+ end
+
+ context 'filtering by group' do
+ it 'includes all merge requests when user has access excluding merge requests from projects the user does not have access to' do
+ private_project = allow_gitaly_n_plus_1 { create(:project, :private, group: group) }
+ private_project.add_guest(user)
+ create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project)
+ params = { group_id: group.id }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2)
+ end
+
+ it 'filters by group including subgroups', :nested_groups do
+ params = { group_id: group.id, include_subgroups: true }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request5)
+ end
+ end
+
+ it 'filters by non_archived' do
+ params = { non_archived: true }
merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(3)
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request5)
end
- it 'excludes merge requests from projects the user does not have access to' do
- private_project = create_project_without_n_plus_1(:private, group: group)
- private_mr = create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project)
- params = { group_id: group.id }
+ it 'filters by iid' do
+ params = { project_id: project1.id, iids: merge_request1.iid }
- private_project.add_guest(user)
merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(3)
- expect(merge_requests).not_to include(private_mr)
+ expect(merge_requests).to contain_exactly(merge_request1)
end
- it 'filters by group including subgroups', :nested_groups do
- params = { group_id: group.id, include_subgroups: true }
+ it 'filters by source branch' do
+ params = { source_branch: merge_request2.source_branch }
merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(6)
+ expect(merge_requests).to contain_exactly(merge_request2)
end
- end
- it 'filters by non_archived' do
- params = { non_archived: true }
- merge_requests = described_class.new(user, params).execute
- expect(merge_requests.size).to eq(8)
- end
+ it 'filters by target branch' do
+ params = { target_branch: merge_request2.target_branch }
- it 'filters by iid' do
- params = { project_id: project1.id, iids: merge_request1.iid }
+ merge_requests = described_class.new(user, params).execute
- merge_requests = described_class.new(user, params).execute
+ expect(merge_requests).to contain_exactly(merge_request2)
+ end
- expect(merge_requests).to contain_exactly(merge_request1)
- end
+ it 'filters by source project id' do
+ params = { source_project_id: merge_request2.source_project_id }
- it 'filters by source branch' do
- params = { source_branch: merge_request2.source_branch }
+ merge_requests = described_class.new(user, params).execute
- merge_requests = described_class.new(user, params).execute
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3)
+ end
- expect(merge_requests).to contain_exactly(merge_request2)
- end
+ it 'filters by state' do
+ params = { state: 'locked' }
- it 'filters by target branch' do
- params = { target_branch: merge_request2.target_branch }
+ merge_requests = described_class.new(user, params).execute
- merge_requests = described_class.new(user, params).execute
+ expect(merge_requests).to contain_exactly(merge_request3)
+ end
- expect(merge_requests).to contain_exactly(merge_request2)
- end
+ describe 'WIP state' do
+ let!(:wip_merge_request1) { create(:merge_request, :simple, author: user, source_project: project5, target_project: project5, title: 'WIP: thing') }
+ let!(:wip_merge_request2) { create(:merge_request, :simple, author: user, source_project: project6, target_project: project6, title: 'wip thing') }
+ let!(:wip_merge_request3) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project1, title: '[wip] thing') }
+ let!(:wip_merge_request4) { create(:merge_request, :simple, author: user, source_project: project1, target_project: project2, title: 'wip: thing') }
- it 'filters by state' do
- params = { state: 'locked' }
+ it 'filters by wip' do
+ params = { wip: 'yes' }
- merge_requests = described_class.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests).to contain_exactly(merge_request3)
- end
+ expect(merge_requests).to contain_exactly(merge_request4, merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4)
+ end
- it 'filters by wip' do
- params = { wip: 'yes' }
+ it 'filters by not wip' do
+ params = { wip: 'no' }
- merge_requests = described_class.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests).to contain_exactly(merge_request4, merge_request5, merge_request6, merge_request7, merge_request8, merge_request9)
- end
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3)
+ end
- it 'filters by not wip' do
- params = { wip: 'no' }
+ it 'returns all items if no valid wip param exists' do
+ params = { wip: '' }
- merge_requests = described_class.new(user, params).execute
+ merge_requests = described_class.new(user, params).execute
- expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3)
- end
+ expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5, wip_merge_request1, wip_merge_request2, wip_merge_request3, wip_merge_request4)
+ end
- it 'returns all items if no valid wip param exists' do
- params = { wip: '' }
+ it 'adds wip to scalar params' do
+ scalar_params = described_class.scalar_params
- merge_requests = described_class.new(user, params).execute
+ expect(scalar_params).to include(:wip, :assignee_id)
+ end
+ end
- expect(merge_requests).to contain_exactly(merge_request1, merge_request2, merge_request3, merge_request4, merge_request5, merge_request6, merge_request7, merge_request8, merge_request9)
- end
+ context 'assignee filtering' do
+ let(:issuables) { described_class.new(user, params).execute }
- it 'adds wip to scalar params' do
- scalar_params = described_class.scalar_params
+ it_behaves_like 'assignee ID filter' do
+ let(:params) { { assignee_id: user.id } }
+ let(:expected_issuables) { [merge_request1, merge_request2] }
+ end
- expect(scalar_params).to include(:wip, :assignee_id)
- end
+ it_behaves_like 'assignee username filter' do
+ before do
+ project2.add_developer(user3)
+ merge_request3.assignees = [user2, user3]
+ end
- context 'filtering by group milestone' do
- let!(:group) { create(:group, :public) }
- let(:group_milestone) { create(:milestone, group: group) }
- let!(:group_member) { create(:group_member, group: group, user: user) }
- let(:params) { { milestone_title: group_milestone.title } }
+ set(:user3) { create(:user) }
+ let(:params) { { assignee_username: [user2.username, user3.username] } }
+ let(:expected_issuables) { [merge_request3] }
+ end
- before do
- project2.update(namespace: group)
- merge_request2.update(milestone: group_milestone)
- merge_request3.update(milestone: group_milestone)
- end
+ it_behaves_like 'no assignee filter' do
+ set(:user3) { create(:user) }
+ let(:expected_issuables) { [merge_request4, merge_request5] }
+ end
- it 'returns issues assigned to that group milestone' do
- merge_requests = described_class.new(user, params).execute
+ it_behaves_like 'any assignee filter' do
+ let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
+ end
- expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
- end
- end
+ context 'filtering by group milestone' do
+ let(:group_milestone) { create(:milestone, group: group) }
- context 'filtering by created_at/updated_at' do
- let(:new_project) { create(:project, forked_from_project: project1) }
-
- let!(:new_merge_request) do
- create(:merge_request,
- :simple,
- author: user,
- created_at: 1.week.from_now,
- updated_at: 1.week.from_now,
- source_project: new_project,
- target_project: new_project)
- end
+ before do
+ project2.update(namespace: group)
+ merge_request2.update(milestone: group_milestone)
+ merge_request3.update(milestone: group_milestone)
+ end
- let!(:old_merge_request) do
- create(:merge_request,
- :simple,
- author: user,
- source_branch: 'feature_1',
- created_at: 1.week.ago,
- updated_at: 1.week.ago,
- source_project: new_project,
- target_project: new_project)
- end
+ it 'returns merge requests assigned to that group milestone' do
+ params = { milestone_title: group_milestone.title }
- before do
- new_project.add_maintainer(user)
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request2, merge_request3)
+ end
+ end
end
- it 'filters by created_after' do
- params = { project_id: new_project.id, created_after: new_merge_request.created_at }
+ context 'filtering by created_at/updated_at' do
+ let(:new_project) { create(:project, forked_from_project: project1) }
- merge_requests = described_class.new(user, params).execute
+ let!(:new_merge_request) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ created_at: 1.week.from_now,
+ updated_at: 1.week.from_now,
+ source_project: new_project,
+ target_project: new_project)
+ end
- expect(merge_requests).to contain_exactly(new_merge_request)
- end
+ let!(:old_merge_request) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_branch: 'feature_1',
+ created_at: 1.week.ago,
+ updated_at: 1.week.ago,
+ source_project: new_project,
+ target_project: new_project)
+ end
- it 'filters by created_before' do
- params = { project_id: new_project.id, created_before: old_merge_request.created_at }
+ before do
+ new_project.add_maintainer(user)
+ end
- merge_requests = described_class.new(user, params).execute
+ it 'filters by created_after' do
+ params = { project_id: new_project.id, created_after: new_merge_request.created_at }
- expect(merge_requests).to contain_exactly(old_merge_request)
- end
+ merge_requests = described_class.new(user, params).execute
- it 'filters by created_after and created_before' do
- params = {
- project_id: new_project.id,
- created_after: old_merge_request.created_at,
- created_before: new_merge_request.created_at
- }
+ expect(merge_requests).to contain_exactly(new_merge_request)
+ end
- merge_requests = described_class.new(user, params).execute
+ it 'filters by created_before' do
+ params = { project_id: new_project.id, created_before: old_merge_request.created_at }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(old_merge_request)
+ end
+
+ it 'filters by created_after and created_before' do
+ params = {
+ project_id: new_project.id,
+ created_after: old_merge_request.created_at,
+ created_before: new_merge_request.created_at
+ }
- expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request)
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request)
+ end
+
+ it 'filters by updated_after' do
+ params = { project_id: new_project.id, updated_after: new_merge_request.updated_at }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(new_merge_request)
+ end
+
+ it 'filters by updated_before' do
+ params = { project_id: new_project.id, updated_before: old_merge_request.updated_at }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(old_merge_request)
+ end
+
+ it 'filters by updated_after and updated_before' do
+ params = {
+ project_id: new_project.id,
+ updated_after: old_merge_request.updated_at,
+ updated_before: new_merge_request.updated_at
+ }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request)
+ end
end
+ end
- it 'filters by updated_after' do
- params = { project_id: new_project.id, updated_after: new_merge_request.updated_at }
+ describe '#row_count', :request_store do
+ it 'returns the number of rows for the default state' do
+ finder = described_class.new(user)
- merge_requests = described_class.new(user, params).execute
+ expect(finder.row_count).to eq(3)
+ end
+
+ it 'returns the number of rows for a given state' do
+ finder = described_class.new(user, state: 'closed')
- expect(merge_requests).to contain_exactly(new_merge_request)
+ expect(finder.row_count).to eq(1)
end
+ end
- it 'filters by updated_before' do
- params = { project_id: new_project.id, updated_before: old_merge_request.updated_at }
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:merge_request, source_project: project) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
+ end
- merge_requests = described_class.new(user, params).execute
+ context 'when projects require different access levels for merge requests' do
+ let(:user) { create(:user) }
+
+ let(:public_project) { create(:project, :public) }
+ let(:internal) { create(:project, :internal) }
+ let(:private_project) { create(:project, :private) }
+ let(:public_with_private_repo) { create(:project, :public, :repository, :repository_private) }
+ let(:internal_with_private_repo) { create(:project, :internal, :repository, :repository_private) }
+
+ let(:merge_requests) { described_class.new(user, {}).execute }
+
+ let!(:mr_public) { create(:merge_request, source_project: public_project) }
+ let!(:mr_private) { create(:merge_request, source_project: private_project) }
+ let!(:mr_internal) { create(:merge_request, source_project: internal) }
+ let!(:mr_private_repo_access) { create(:merge_request, source_project: public_with_private_repo) }
+ let!(:mr_internal_private_repo_access) { create(:merge_request, source_project: internal_with_private_repo) }
- expect(merge_requests).to contain_exactly(old_merge_request)
+ context 'with admin user' do
+ let(:user) { create(:user, :admin) }
+
+ it 'returns all merge requests' do
+ expect(merge_requests).to eq(
+ [mr_internal_private_repo_access, mr_private_repo_access, mr_internal, mr_private, mr_public]
+ )
end
+ end
- it 'filters by updated_after and updated_before' do
- params = {
- project_id: new_project.id,
- updated_after: old_merge_request.updated_at,
- updated_before: new_merge_request.updated_at
- }
+ context 'when project restricts merge requests' do
+ let(:non_member) { create(:user) }
+ let(:project) { create(:project, :repository, :public, :merge_requests_private) }
+ let!(:merge_request) { create(:merge_request, source_project: project) }
- merge_requests = described_class.new(user, params).execute
+ it "returns nothing to to non members" do
+ merge_requests = described_class.new(
+ non_member,
+ project_id: project.id
+ ).execute
- expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request)
+ expect(merge_requests).to be_empty
end
end
- end
- describe '#row_count', :request_store do
- it 'returns the number of rows for the default state' do
- finder = described_class.new(user)
+ context 'with external user' do
+ let(:user) { create(:user, :external) }
- expect(finder.row_count).to eq(7)
+ it 'returns only public merge requests' do
+ expect(merge_requests).to eq([mr_public])
+ end
end
- it 'returns the number of rows for a given state' do
- finder = described_class.new(user, state: 'closed')
+ context 'with authenticated user' do
+ it 'returns public and internal merge requests' do
+ expect(merge_requests).to eq([mr_internal, mr_public])
+ end
- expect(finder.row_count).to eq(1)
+ context 'being added to the private project' do
+ context 'as a guest' do
+ before do
+ private_project.add_guest(user)
+ end
+
+ it 'does not return merge requests from the private project' do
+ expect(merge_requests).to eq([mr_internal, mr_public])
+ end
+ end
+
+ context 'as a developer' do
+ before do
+ private_project.add_developer(user)
+ end
+
+ it 'returns merge requests from the private project' do
+ expect(merge_requests).to eq([mr_internal, mr_private, mr_public])
+ end
+ end
+ end
+
+ context 'being added to the public project with private repo access' do
+ context 'as a guest' do
+ before do
+ public_with_private_repo.add_guest(user)
+ end
+
+ it 'returns merge requests from the project' do
+ expect(merge_requests).to eq([mr_internal, mr_public])
+ end
+ end
+
+ context 'as a reporter' do
+ before do
+ public_with_private_repo.add_reporter(user)
+ end
+
+ it 'returns merge requests from the project' do
+ expect(merge_requests).to eq([mr_private_repo_access, mr_internal, mr_public])
+ end
+ end
+ end
+
+ context 'being added to the internal project with private repo access' do
+ context 'as a guest' do
+ before do
+ internal_with_private_repo.add_guest(user)
+ end
+
+ it 'returns merge requests from the project' do
+ expect(merge_requests).to eq([mr_internal, mr_public])
+ end
+ end
+
+ context 'as a reporter' do
+ before do
+ internal_with_private_repo.add_reporter(user)
+ end
+
+ it 'returns merge requests from the project' do
+ expect(merge_requests).to eq([mr_internal_private_repo_access, mr_internal, mr_public])
+ end
+ end
+ end
end
end
end
diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb
index ecffbb9e197..34c7b508c56 100644
--- a/spec/finders/milestones_finder_spec.rb
+++ b/spec/finders/milestones_finder_spec.rb
@@ -9,7 +9,7 @@ describe MilestonesFinder do
let!(:milestone_3) { create(:milestone, project: project_1, state: 'active', due_date: Date.tomorrow) }
let!(:milestone_4) { create(:milestone, project: project_2, state: 'active') }
- it 'it returns milestones for projects' do
+ it 'returns milestones for projects' do
result = described_class.new(project_ids: [project_1.id, project_2.id], state: 'all').execute
expect(result).to contain_exactly(milestone_3, milestone_4)
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 35279906854..8aea45b457c 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -4,12 +4,13 @@ require 'spec_helper'
describe Projects::Serverless::FunctionsFinder do
include KubernetesHelpers
+ include PrometheusHelpers
include ReactiveCachingHelpers
let(:user) { create(:user) }
let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
let(:service) { cluster.platform_kubernetes }
- let(:project) { cluster.project}
+ let(:project) { cluster.project }
let(:namespace) do
create(:cluster_kubernetes_namespace,
@@ -22,14 +23,50 @@ describe Projects::Serverless::FunctionsFinder do
project.add_maintainer(user)
end
+ describe '#installed' do
+ it 'when reactive_caching is still fetching data' do
+ expect(described_class.new(project).knative_installed).to eq 'checking'
+ end
+
+ context 'when reactive_caching has finished' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+ before do
+ allow_any_instance_of(Clusters::Cluster)
+ .to receive(:knative_services_finder)
+ .and_return(knative_services_finder)
+ synchronous_reactive_cache(knative_services_finder)
+ end
+
+ context 'when knative is not installed' do
+ it 'returns false' do
+ stub_kubeclient_discover_knative_not_found(service.api_url)
+
+ expect(described_class.new(project).knative_installed).to eq false
+ end
+ end
+
+ context 'reactive_caching is finished and knative is installed' do
+ let(:knative_services_finder) { project.clusters.first.knative_services_finder(project) }
+
+ it 'returns true' do
+ stub_kubeclient_knative_services(namespace: namespace.namespace)
+ stub_kubeclient_service_pods(nil, namespace: namespace.namespace)
+
+ expect(described_class.new(project).knative_installed).to be true
+ end
+ end
+ end
+ end
+
describe 'retrieve data from knative' do
- it 'does not have knative installed' do
- expect(described_class.new(project.clusters).execute).to be_empty
+ context 'does not have knative installed' do
+ it { expect(described_class.new(project).execute).to be_empty }
end
context 'has knative installed' do
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
- let(:finder) { described_class.new(project.clusters) }
+ let(:finder) { described_class.new(project) }
it 'there are no functions' do
expect(finder.execute).to be_empty
@@ -37,42 +74,51 @@ describe Projects::Serverless::FunctionsFinder do
it 'there are functions', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
expect(finder.execute).not_to be_empty
end
it 'has a function', :use_clean_rails_memory_store_caching do
stub_kubeclient_service_pods
- stub_reactive_cache(knative,
+ stub_reactive_cache(cluster.knative_services_finder(project),
{
services: kube_knative_services_body(namespace: namespace.namespace, name: cluster.project.name)["items"],
pods: kube_knative_pods_body(cluster.project.name, namespace.namespace)["items"]
- })
+ },
+ *cluster.knative_services_finder(project).cache_args)
result = finder.service(cluster.environment_scope, cluster.project.name)
expect(result).not_to be_empty
expect(result["metadata"]["name"]).to be_eql(cluster.project.name)
end
- end
- end
- describe 'verify if knative is installed' do
- context 'knative is not installed' do
- it 'does not have knative installed' do
- expect(described_class.new(project.clusters).installed?).to be false
+ it 'has metrics', :use_clean_rails_memory_store_caching do
end
end
- context 'knative is installed' do
+ context 'has prometheus' do
+ let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) }
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let(:finder) { described_class.new(project) }
+
+ before do
+ allow(finder).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:query).and_return(prometheus_empty_body('matrix'))
+ end
+
+ it 'is available' do
+ expect(finder.has_prometheus?("*")).to be true
+ end
- it 'does have knative installed' do
- expect(described_class.new(project.clusters).installed?).to be true
+ it 'has query data' do
+ expect(finder.invocation_metrics("*", cluster.project.name)).not_to be_nil
end
end
end
diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb
index 134fb5f2c04..d367f9015c7 100644
--- a/spec/finders/snippets_finder_spec.rb
+++ b/spec/finders/snippets_finder_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe SnippetsFinder do
+ include ExternalAuthorizationServiceHelpers
include Gitlab::Allowable
- using RSpec::Parameterized::TableSyntax
describe '#initialize' do
it 'raises ArgumentError when a project and author are given' do
@@ -14,174 +14,142 @@ describe SnippetsFinder do
end
end
- context 'filter by scope' do
- let(:user) { create :user }
- let!(:snippet1) { create(:personal_snippet, :private, author: user) }
- let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
- let!(:snippet3) { create(:personal_snippet, :public, author: user) }
-
- it "returns all snippets for 'all' scope" do
- snippets = described_class.new(user, scope: :all).execute
-
- expect(snippets).to include(snippet1, snippet2, snippet3)
- end
-
- it "returns all snippets for 'are_private' scope" do
- snippets = described_class.new(user, scope: :are_private).execute
+ describe '#execute' do
+ set(:user) { create(:user) }
+ set(:private_personal_snippet) { create(:personal_snippet, :private, author: user) }
+ set(:internal_personal_snippet) { create(:personal_snippet, :internal, author: user) }
+ set(:public_personal_snippet) { create(:personal_snippet, :public, author: user) }
- expect(snippets).to include(snippet1)
- expect(snippets).not_to include(snippet2, snippet3)
- end
+ context 'filter by scope' do
+ it "returns all snippets for 'all' scope" do
+ snippets = described_class.new(user, scope: :all).execute
- it "returns all snippets for 'are_internal' scope" do
- snippets = described_class.new(user, scope: :are_internal).execute
+ expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
+ end
- expect(snippets).to include(snippet2)
- expect(snippets).not_to include(snippet1, snippet3)
- end
+ it "returns all snippets for 'are_private' scope" do
+ snippets = described_class.new(user, scope: :are_private).execute
- it "returns all snippets for 'are_private' scope" do
- snippets = described_class.new(user, scope: :are_public).execute
+ expect(snippets).to contain_exactly(private_personal_snippet)
+ end
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet1, snippet2)
- end
- end
+ it "returns all snippets for 'are_internal' scope" do
+ snippets = described_class.new(user, scope: :are_internal).execute
- context 'filter by author' do
- let(:user) { create :user }
- let(:user1) { create :user }
- let!(:snippet1) { create(:personal_snippet, :private, author: user) }
- let!(:snippet2) { create(:personal_snippet, :internal, author: user) }
- let!(:snippet3) { create(:personal_snippet, :public, author: user) }
+ expect(snippets).to contain_exactly(internal_personal_snippet)
+ end
- it "returns all public and internal snippets" do
- snippets = described_class.new(user1, author: user).execute
+ it "returns all snippets for 'are_private' scope" do
+ snippets = described_class.new(user, scope: :are_public).execute
- expect(snippets).to include(snippet2, snippet3)
- expect(snippets).not_to include(snippet1)
+ expect(snippets).to contain_exactly(public_personal_snippet)
+ end
end
- it "returns internal snippets" do
- snippets = described_class.new(user, author: user, scope: :are_internal).execute
+ context 'filter by author' do
+ it 'returns all public and internal snippets' do
+ snippets = described_class.new(create(:user), author: user).execute
- expect(snippets).to include(snippet2)
- expect(snippets).not_to include(snippet1, snippet3)
- end
+ expect(snippets).to contain_exactly(internal_personal_snippet, public_personal_snippet)
+ end
- it "returns private snippets" do
- snippets = described_class.new(user, author: user, scope: :are_private).execute
+ it 'returns internal snippets' do
+ snippets = described_class.new(user, author: user, scope: :are_internal).execute
- expect(snippets).to include(snippet1)
- expect(snippets).not_to include(snippet2, snippet3)
- end
+ expect(snippets).to contain_exactly(internal_personal_snippet)
+ end
- it "returns public snippets" do
- snippets = described_class.new(user, author: user, scope: :are_public).execute
+ it 'returns private snippets' do
+ snippets = described_class.new(user, author: user, scope: :are_private).execute
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet1, snippet2)
- end
+ expect(snippets).to contain_exactly(private_personal_snippet)
+ end
- it "returns all snippets" do
- snippets = described_class.new(user, author: user).execute
+ it 'returns public snippets' do
+ snippets = described_class.new(user, author: user, scope: :are_public).execute
- expect(snippets).to include(snippet1, snippet2, snippet3)
- end
+ expect(snippets).to contain_exactly(public_personal_snippet)
+ end
- it "returns only public snippets if unauthenticated user" do
- snippets = described_class.new(nil, author: user).execute
+ it 'returns all snippets' do
+ snippets = described_class.new(user, author: user).execute
- expect(snippets).to include(snippet3)
- expect(snippets).not_to include(snippet2, snippet1)
- end
+ expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
+ end
- it 'returns all snippets for an admin' do
- admin = create(:user, :admin)
- snippets = described_class.new(admin, author: user).execute
+ it 'returns only public snippets if unauthenticated user' do
+ snippets = described_class.new(nil, author: user).execute
- expect(snippets).to include(snippet1, snippet2, snippet3)
- end
- end
+ expect(snippets).to contain_exactly(public_personal_snippet)
+ end
- context 'filter by project' do
- let(:user) { create :user }
- let(:group) { create :group, :public }
- let(:project1) { create(:project, :public, group: group) }
+ it 'returns all snippets for an admin' do
+ admin = create(:user, :admin)
+ snippets = described_class.new(admin, author: user).execute
- before do
- @snippet1 = create(:project_snippet, :private, project: project1)
- @snippet2 = create(:project_snippet, :internal, project: project1)
- @snippet3 = create(:project_snippet, :public, project: project1)
+ expect(snippets).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
+ end
end
- it "returns public snippets for unauthorized user" do
- snippets = described_class.new(nil, project: project1).execute
+ context 'project snippets' do
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, group: group) }
+ let!(:private_project_snippet) { create(:project_snippet, :private, project: project) }
+ let!(:internal_project_snippet) { create(:project_snippet, :internal, project: project) }
+ let!(:public_project_snippet) { create(:project_snippet, :public, project: project) }
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet1, @snippet2)
- end
-
- it "returns public and internal snippets for non project members" do
- snippets = described_class.new(user, project: project1).execute
+ it 'returns public personal and project snippets for unauthorized user' do
+ snippets = described_class.new(nil, project: project).execute
- expect(snippets).to include(@snippet2, @snippet3)
- expect(snippets).not_to include(@snippet1)
- end
+ expect(snippets).to contain_exactly(public_project_snippet)
+ end
- it "returns public snippets for non project members" do
- snippets = described_class.new(user, project: project1, scope: :are_public).execute
+ it 'returns public and internal snippets for non project members' do
+ snippets = described_class.new(user, project: project).execute
- expect(snippets).to include(@snippet3)
- expect(snippets).not_to include(@snippet1, @snippet2)
- end
+ expect(snippets).to contain_exactly(internal_project_snippet, public_project_snippet)
+ end
- it "returns internal snippets for non project members" do
- snippets = described_class.new(user, project: project1, scope: :are_internal).execute
+ it 'returns public snippets for non project members' do
+ snippets = described_class.new(user, project: project, scope: :are_public).execute
- expect(snippets).to include(@snippet2)
- expect(snippets).not_to include(@snippet1, @snippet3)
- end
+ expect(snippets).to contain_exactly(public_project_snippet)
+ end
- it "does not return private snippets for non project members" do
- snippets = described_class.new(user, project: project1, scope: :are_private).execute
+ it 'returns internal snippets for non project members' do
+ snippets = described_class.new(user, project: project, scope: :are_internal).execute
- expect(snippets).not_to include(@snippet1, @snippet2, @snippet3)
- end
+ expect(snippets).to contain_exactly(internal_project_snippet)
+ end
- it "returns all snippets for project members" do
- project1.add_developer(user)
+ it 'does not return private snippets for non project members' do
+ snippets = described_class.new(user, project: project, scope: :are_private).execute
- snippets = described_class.new(user, project: project1).execute
+ expect(snippets).to be_empty
+ end
- expect(snippets).to include(@snippet1, @snippet2, @snippet3)
- end
+ it 'returns all snippets for project members' do
+ project.add_developer(user)
- it "returns private snippets for project members" do
- project1.add_developer(user)
+ snippets = described_class.new(user, project: project).execute
- snippets = described_class.new(user, project: project1, scope: :are_private).execute
+ expect(snippets).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet)
+ end
- expect(snippets).to include(@snippet1)
- end
+ it 'returns private snippets for project members' do
+ project.add_developer(user)
- it 'returns all snippets for an admin' do
- admin = create(:user, :admin)
- snippets = described_class.new(admin, project: project1).execute
+ snippets = described_class.new(user, project: project, scope: :are_private).execute
- expect(snippets).to include(@snippet1, @snippet2, @snippet3)
- end
- end
+ expect(snippets).to contain_exactly(private_project_snippet)
+ end
- describe '#execute' do
- let(:project) { create(:project, :public) }
- let!(:project_snippet) { create(:project_snippet, :public, project: project) }
- let!(:personal_snippet) { create(:personal_snippet, :public) }
- let(:user) { create(:user) }
- subject(:finder) { described_class.new(user) }
+ it 'returns all snippets for an admin' do
+ admin = create(:user, :admin)
+ snippets = described_class.new(admin, project: project).execute
- it 'returns project- and personal snippets' do
- expect(finder.execute).to contain_exactly(project_snippet, personal_snippet)
+ expect(snippets).to contain_exactly(private_project_snippet, internal_project_snippet, public_project_snippet)
+ end
end
context 'when the user cannot read cross project' do
@@ -191,10 +159,41 @@ describe SnippetsFinder do
end
it 'returns only personal snippets when the user cannot read cross project' do
- expect(finder.execute).to contain_exactly(personal_snippet)
+ expect(described_class.new(user).execute).to contain_exactly(private_personal_snippet, internal_personal_snippet, public_personal_snippet)
end
end
end
it_behaves_like 'snippet visibility'
+
+ context 'external authorization' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:snippet) { create(:project_snippet, :public, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:project_snippet, project: project) }
+ let(:project_params) { { project: project } }
+ end
+
+ it 'includes the result if the external service allows access' do
+ external_service_allow_access(user, project)
+
+ results = described_class.new(user, project: project).execute
+
+ expect(results).to contain_exactly(snippet)
+ end
+
+ it 'does not include any results if the external service denies access' do
+ external_service_deny_access(user, project)
+
+ results = described_class.new(user, project: project).execute
+
+ expect(results).to be_empty
+ end
+ end
end
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index d4ed41d54f0..22318a9946a 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -47,6 +47,13 @@ describe TodosFinder do
end
end
end
+
+ context 'external authorization' do
+ it_behaves_like 'a finder with external authorization service' do
+ let!(:subject) { create(:todo, project: project, user: user) }
+ let(:project_params) { { project_id: project.id } }
+ end
+ end
end
describe '#sort' do
diff --git a/spec/finders/users_finder_spec.rb b/spec/finders/users_finder_spec.rb
index fecf97dc641..d71d3c99272 100644
--- a/spec/finders/users_finder_spec.rb
+++ b/spec/finders/users_finder_spec.rb
@@ -2,10 +2,7 @@ require 'spec_helper'
describe UsersFinder do
describe '#execute' do
- let!(:user1) { create(:user, username: 'johndoe') }
- let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
- let!(:external_user) { create(:user, :external) }
- let!(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+ include_context 'UsersFinder#execute filter by project context'
context 'with a normal user' do
let(:user) { create(:user) }
@@ -13,43 +10,43 @@ describe UsersFinder do
it 'returns all users' do
users = described_class.new(user).execute
- expect(users).to contain_exactly(user, user1, user2, omniauth_user)
+ expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user)
end
it 'filters by username' do
users = described_class.new(user, username: 'johndoe').execute
- expect(users).to contain_exactly(user1)
+ expect(users).to contain_exactly(normal_user)
end
it 'filters by username (case insensitive)' do
users = described_class.new(user, username: 'joHNdoE').execute
- expect(users).to contain_exactly(user1)
+ expect(users).to contain_exactly(normal_user)
end
it 'filters by search' do
users = described_class.new(user, search: 'orando').execute
- expect(users).to contain_exactly(user2)
+ expect(users).to contain_exactly(blocked_user)
end
it 'filters by blocked users' do
users = described_class.new(user, blocked: true).execute
- expect(users).to contain_exactly(user2)
+ expect(users).to contain_exactly(blocked_user)
end
it 'filters by active users' do
users = described_class.new(user, active: true).execute
- expect(users).to contain_exactly(user, user1, omniauth_user)
+ expect(users).to contain_exactly(user, normal_user, omniauth_user)
end
it 'returns no external users' do
users = described_class.new(user, external: true).execute
- expect(users).to contain_exactly(user, user1, user2, omniauth_user)
+ expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user)
end
it 'filters by created_at' do
@@ -69,7 +66,7 @@ describe UsersFinder do
custom_attributes: { foo: 'bar' }
).execute
- expect(users).to contain_exactly(user, user1, user2, omniauth_user)
+ expect(users).to contain_exactly(user, normal_user, blocked_user, omniauth_user)
end
end
@@ -85,20 +82,20 @@ describe UsersFinder do
it 'returns all users' do
users = described_class.new(admin).execute
- expect(users).to contain_exactly(admin, user1, user2, external_user, omniauth_user)
+ expect(users).to contain_exactly(admin, normal_user, blocked_user, external_user, omniauth_user)
end
it 'filters by custom attributes' do
- create :user_custom_attribute, user: user1, key: 'foo', value: 'foo'
- create :user_custom_attribute, user: user1, key: 'bar', value: 'bar'
- create :user_custom_attribute, user: user2, key: 'foo', value: 'foo'
+ create :user_custom_attribute, user: normal_user, key: 'foo', value: 'foo'
+ create :user_custom_attribute, user: normal_user, key: 'bar', value: 'bar'
+ create :user_custom_attribute, user: blocked_user, key: 'foo', value: 'foo'
users = described_class.new(
admin,
custom_attributes: { foo: 'foo', bar: 'bar' }
).execute
- expect(users).to contain_exactly(user1)
+ expect(users).to contain_exactly(normal_user)
end
end
end
diff --git a/spec/fixtures/api/graphql/introspection.graphql b/spec/fixtures/api/graphql/introspection.graphql
new file mode 100644
index 00000000000..7b712068fcd
--- /dev/null
+++ b/spec/fixtures/api/graphql/introspection.graphql
@@ -0,0 +1,92 @@
+# pulled from GraphiQL query
+query IntrospectionQuery {
+ __schema {
+ queryType { name }
+ mutationType { name }
+ subscriptionType { name }
+ types {
+ ...FullType
+ }
+ directives {
+ name
+ description
+ locations
+ args {
+ ...InputValue
+ }
+ }
+ }
+}
+
+fragment FullType on __Type {
+ kind
+ name
+ description
+ fields(includeDeprecated: true) {
+ name
+ description
+ args {
+ ...InputValue
+ }
+ type {
+ ...TypeRef
+ }
+ isDeprecated
+ deprecationReason
+ }
+ inputFields {
+ ...InputValue
+ }
+ interfaces {
+ ...TypeRef
+ }
+ enumValues(includeDeprecated: true) {
+ name
+ description
+ isDeprecated
+ deprecationReason
+ }
+ possibleTypes {
+ ...TypeRef
+ }
+}
+
+fragment InputValue on __InputValue {
+ name
+ description
+ type { ...TypeRef }
+ defaultValue
+}
+
+fragment TypeRef on __Type {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/board.json b/spec/fixtures/api/schemas/board.json
index 03aca4a3cc0..7c146647948 100644
--- a/spec/fixtures/api/schemas/board.json
+++ b/spec/fixtures/api/schemas/board.json
@@ -6,6 +6,5 @@
"properties" : {
"id": { "type": "integer" },
"name": { "type": "string" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 5ebc09a96dc..695175689b9 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -33,9 +33,11 @@
"version": { "type": "string" },
"status_reason": { "type": ["string", "null"] },
"external_ip": { "type": ["string", "null"] },
+ "external_hostname": { "type": ["string", "null"] },
"hostname": { "type": ["string", "null"] },
"email": { "type": ["string", "null"] },
- "update_available": { "type": ["boolean", "null"] }
+ "update_available": { "type": ["boolean", "null"] },
+ "can_uninstall": { "type": "boolean" }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/fixtures/api/schemas/entities/issue.json b/spec/fixtures/api/schemas/entities/issue.json
index 00abe73ec8a..9898819ef75 100644
--- a/spec/fixtures/api/schemas/entities/issue.json
+++ b/spec/fixtures/api/schemas/entities/issue.json
@@ -5,7 +5,7 @@
"iid": { "type": "integer" },
"author_id": { "type": "integer" },
"description": { "type": ["string", "null"] },
- "lock_version": { "type": ["string", "null"] },
+ "lock_version": { "type": ["integer", "null"] },
"milestone_id": { "type": ["string", "null"] },
"title": { "type": "string" },
"moved_to_id": { "type": ["integer", "null"] },
@@ -38,6 +38,5 @@
"items": { "$ref": "label.json" }
},
"assignees": { "type": ["array", "null"] }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json
index f7b270ffa8d..7cb65e1f2f5 100644
--- a/spec/fixtures/api/schemas/entities/issue_board.json
+++ b/spec/fixtures/api/schemas/entities/issue_board.json
@@ -9,6 +9,9 @@
"project_id": { "type": "integer" },
"relative_position": { "type": ["integer", "null"] },
"time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["string", "null"] },
+ "human_total_time_spent": { "type": ["string", "null"] },
"weight": { "type": ["integer", "null"] },
"project": {
"type": "object",
diff --git a/spec/fixtures/api/schemas/entities/issue_boards.json b/spec/fixtures/api/schemas/entities/issue_boards.json
index 0ac1d9468c8..742f7be5485 100644
--- a/spec/fixtures/api/schemas/entities/issue_boards.json
+++ b/spec/fixtures/api/schemas/entities/issue_boards.json
@@ -10,6 +10,5 @@
"items": { "$ref": "issue_board.json" }
},
"size": { "type": "integer" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_basic.json b/spec/fixtures/api/schemas/entities/merge_request_basic.json
index 3006b482d41..ac0a0314455 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_basic.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_basic.json
@@ -6,14 +6,14 @@
"source_branch_exists": { "type": "boolean" },
"merge_error": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" },
- "assignee_id": { "type": ["integer", "null"] },
"allow_collaboration": { "type": "boolean"},
"allow_maintainer_to_push": { "type": "boolean"},
- "assignee": {
- "oneOf": [
- { "type": "null" },
- { "$ref": "user.json" }
- ]
+ "assignees": {
+ "type": ["array"],
+ "items": {
+ "type": "object",
+ "$ref": "../public_api/v4/user/basic.json"
+ }
},
"milestone": {
"type": [ "object", "null" ]
@@ -23,7 +23,7 @@
},
"task_status": { "type": "string" },
"task_status_short": { "type": "string" },
- "lock_version": { "type": ["string", "null"] }
+ "lock_version": { "type": ["integer", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
index 7e9e048a9fd..214b67a9a0f 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_sidebar.json
@@ -51,6 +51,5 @@
"toggle_subscription_path": { "type": "string" },
"move_issue_path": { "type": "string" },
"projects_autocomplete_path": { "type": "string" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index 67c209f3fc3..7018cb9a305 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -52,6 +52,7 @@
"mergeable_discussions_state": { "type": "boolean" },
"conflicts_can_be_resolved_in_ui": { "type": "boolean" },
"branch_missing": { "type": "boolean" },
+ "commits_count": { "type": ["integer", "null"] },
"has_conflicts": { "type": "boolean" },
"can_be_merged": { "type": "boolean" },
"mergeable": { "type": "boolean" },
@@ -124,7 +125,7 @@
"test_reports_path": { "type": ["string", "null"] },
"can_receive_suggestion": { "type": "boolean" },
"source_branch_protected": { "type": "boolean" },
- "conflicts_docs_path": { "type": ["string", "null"] }
- },
- "additionalProperties": false
+ "conflicts_docs_path": { "type": ["string", "null"] },
+ "merge_request_pipelines_docs_path": { "type": ["string", "null"] }
+ }
}
diff --git a/spec/fixtures/api/schemas/entities/test_case.json b/spec/fixtures/api/schemas/entities/test_case.json
index c9ba1f3ad18..70f6edeeeb7 100644
--- a/spec/fixtures/api/schemas/entities/test_case.json
+++ b/spec/fixtures/api/schemas/entities/test_case.json
@@ -7,6 +7,7 @@
"properties": {
"status": { "type": "string" },
"name": { "type": "string" },
+ "classname": { "type": "string" },
"execution_time": { "type": "float" },
"system_output": { "type": ["string", "null"] },
"stack_trace": { "type": ["string", "null"] }
diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json
index f1d33e3ce7b..5b1e3c049fa 100644
--- a/spec/fixtures/api/schemas/environment.json
+++ b/spec/fixtures/api/schemas/environment.json
@@ -20,6 +20,7 @@
"state": { "type": "string" },
"external_url": { "$ref": "types/nullable_string.json" },
"environment_type": { "$ref": "types/nullable_string.json" },
+ "name_without_type": { "type": "string" },
"has_stop_action": { "type": "boolean" },
"environment_path": { "type": "string" },
"stop_path": { "type": "string" },
@@ -30,7 +31,11 @@
"last_deployment": {
"oneOf": [
{ "type": "null" },
- { "$ref": "deployment.json" }
+ { "$ref": "deployment.json" },
+ {
+ "name": { "type": "string" },
+ "build_path": { "type": "string" }
+ }
]
}
},
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index a83ec55cede..77de9ae4f9f 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -28,7 +28,7 @@
"items": { "$ref": "entities/label.json" }
},
"assignee": {
- "id": { "type": "integet" },
+ "id": { "type": "integer" },
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
@@ -52,6 +52,5 @@
}
},
"subscribed": { "type": ["boolean", "null"] }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/issues.json b/spec/fixtures/api/schemas/issues.json
index 70771b21c96..fbcd9eea389 100644
--- a/spec/fixtures/api/schemas/issues.json
+++ b/spec/fixtures/api/schemas/issues.json
@@ -10,6 +10,5 @@
"items": { "$ref": "issue.json" }
},
"size": { "type": "integer" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/pipeline_schedule.json b/spec/fixtures/api/schemas/pipeline_schedule.json
index c76c6945117..690c4a7d4e8 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule.json
@@ -33,7 +33,7 @@
"additionalProperties": false
},
"variables": {
- "type": ["array", "null"],
+ "type": "array",
"items": { "$ref": "pipeline_schedule_variable.json" }
}
},
diff --git a/spec/fixtures/api/schemas/pipeline_schedule_variable.json b/spec/fixtures/api/schemas/pipeline_schedule_variable.json
index f7ccb2d44a0..022d36cb88c 100644
--- a/spec/fixtures/api/schemas/pipeline_schedule_variable.json
+++ b/spec/fixtures/api/schemas/pipeline_schedule_variable.json
@@ -1,8 +1,14 @@
{
- "type": ["object", "null"],
+ "type": "object",
+ "required": [
+ "key",
+ "value",
+ "variable_type"
+ ],
"properties": {
"key": { "type": "string" },
- "value": { "type": "string" }
+ "value": { "type": "string" },
+ "variable_type": { "type": "string" }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact.json b/spec/fixtures/api/schemas/public_api/v4/artifact.json
new file mode 100644
index 00000000000..9df957b1498
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/artifact.json
@@ -0,0 +1,16 @@
+{
+ "type": "object",
+ "required": [
+ "file_type",
+ "size",
+ "filename",
+ "file_format"
+ ],
+ "properties": {
+ "file_type": { "type": "string"},
+ "size": { "type": "integer"},
+ "filename": { "type": "string"},
+ "file_format": { "type": "string"}
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/artifact_file.json b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json
new file mode 100644
index 00000000000..4017e6bdabc
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/artifact_file.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required": [
+ "filename",
+ "size"
+ ],
+ "properties": {
+ "filename": { "type": "string"},
+ "size": { "type": "integer"}
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/deployment.json b/spec/fixtures/api/schemas/public_api/v4/deployment.json
new file mode 100644
index 00000000000..3af2dc27d55
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/deployment.json
@@ -0,0 +1,32 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "iid",
+ "ref",
+ "sha",
+ "created_at",
+ "user",
+ "deployable"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "ref": { "type": "string" },
+ "sha": { "type": "string" },
+ "created_at": { "type": "string" },
+ "user": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "user/basic.json" }
+ ]
+ },
+ "deployable": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "job.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/environment.json b/spec/fixtures/api/schemas/public_api/v4/environment.json
new file mode 100644
index 00000000000..242e90fb7ac
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/environment.json
@@ -0,0 +1,23 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "slug",
+ "external_url",
+ "last_deployment"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "slug": { "type": "string" },
+ "external_url": { "$ref": "../../types/nullable_string.json" },
+ "last_deployment": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "deployment.json" }
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/job.json b/spec/fixtures/api/schemas/public_api/v4/job.json
new file mode 100644
index 00000000000..c038ae0a664
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/job.json
@@ -0,0 +1,64 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "status",
+ "stage",
+ "name",
+ "ref",
+ "tag",
+ "coverage",
+ "created_at",
+ "started_at",
+ "finished_at",
+ "duration",
+ "user",
+ "commit",
+ "pipeline",
+ "web_url",
+ "artifacts",
+ "artifacts_expire_at",
+ "runner"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "status": { "type": "string" },
+ "stage": { "type": "string" },
+ "name": { "type": "string" },
+ "ref": { "type": "string" },
+ "tag": { "type": "boolean" },
+ "coverage": { "type": ["number", "null"] },
+ "allow_failure": { "type": "boolean" },
+ "created_at": { "type": "string" },
+ "started_at": { "type": ["null", "string"] },
+ "finished_at": { "type": ["null", "string"] },
+ "duration": { "type": ["null", "number"] },
+ "user": { "$ref": "user/basic.json" },
+ "commit": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "commit/basic.json" }
+ ]
+ },
+ "pipeline": { "$ref": "pipeline/basic.json" },
+ "web_url": { "type": "string" },
+ "artifacts": {
+ "type": "array",
+ "items": { "$ref": "artifact.json" }
+ },
+ "artifacts_file": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "artifact_file.json" }
+ ]
+ },
+ "artifacts_expire_at": { "type": ["null", "string"] },
+ "runner": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "runner.json" }
+ ]
+ }
+ },
+ "additionalProperties":false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/label_basic.json b/spec/fixtures/api/schemas/public_api/v4/label_basic.json
new file mode 100644
index 00000000000..37bbdcb14fe
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/label_basic.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "name",
+ "color",
+ "description",
+ "text_color"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}$"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_request.json b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
new file mode 100644
index 00000000000..a423bf70b69
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_request.json
@@ -0,0 +1,135 @@
+{
+ "type": "object",
+ "properties" : {
+ "properties" : {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": "integer" },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "merged_by": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "merged_at": { "type": ["date", "null"] },
+ "closed_by": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "closed_at": { "type": ["date", "null"] },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "target_branch": { "type": "string" },
+ "source_branch": { "type": "string" },
+ "upvotes": { "type": "integer" },
+ "downvotes": { "type": "integer" },
+ "author": {
+ "type": "object",
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "assignee": {
+ "type": ["object", "null"],
+ "properties": {
+ "name": { "type": "string" },
+ "username": { "type": "string" },
+ "id": { "type": "integer" },
+ "state": { "type": "string" },
+ "avatar_url": { "type": "uri" },
+ "web_url": { "type": "uri" }
+ },
+ "additionalProperties": false
+ },
+ "assignees": {
+ "items": {
+ "$ref": "./merge_request.json"
+ }
+ },
+ "source_project_id": { "type": "integer" },
+ "target_project_id": { "type": "integer" },
+ "labels": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "work_in_progress": { "type": "boolean" },
+ "milestone": {
+ "type": ["object", "null"],
+ "properties": {
+ "id": { "type": "integer" },
+ "iid": { "type": "integer" },
+ "project_id": { "type": ["integer", "null"] },
+ "group_id": { "type": ["integer", "null"] },
+ "title": { "type": "string" },
+ "description": { "type": ["string", "null"] },
+ "state": { "type": "string" },
+ "created_at": { "type": "date" },
+ "updated_at": { "type": "date" },
+ "due_date": { "type": "date" },
+ "start_date": { "type": "date" }
+ },
+ "additionalProperties": false
+ },
+ "merge_when_pipeline_succeeds": { "type": "boolean" },
+ "merge_status": { "type": "string" },
+ "sha": { "type": "string" },
+ "merge_commit_sha": { "type": ["string", "null"] },
+ "user_notes_count": { "type": "integer" },
+ "changes_count": { "type": "string" },
+ "should_remove_source_branch": { "type": ["boolean", "null"] },
+ "force_remove_source_branch": { "type": ["boolean", "null"] },
+ "discussion_locked": { "type": ["boolean", "null"] },
+ "web_url": { "type": "uri" },
+ "squash": { "type": "boolean" },
+ "time_stats": {
+ "time_estimate": { "type": "integer" },
+ "total_time_spent": { "type": "integer" },
+ "human_time_estimate": { "type": ["string", "null"] },
+ "human_total_time_spent": { "type": ["string", "null"] }
+ },
+ "allow_collaboration": { "type": ["boolean", "null"] },
+ "allow_maintainer_to_push": { "type": ["boolean", "null"] }
+ },
+ "required": [
+ "id", "iid", "project_id", "title", "description",
+ "state", "created_at", "updated_at", "target_branch",
+ "source_branch", "upvotes", "downvotes", "author",
+ "assignee", "source_project_id", "target_project_id",
+ "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
+ "merge_status", "sha", "merge_commit_sha", "user_notes_count",
+ "should_remove_source_branch", "force_remove_source_branch",
+ "web_url", "squash"
+ ],
+ "head_pipeline": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "pipeline/detail.json" }
+ ]
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
index 6df27bf32b9..b35c83950e8 100644
--- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
+++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json
@@ -1,126 +1,6 @@
{
"type": "array",
"items": {
- "type": "object",
- "properties" : {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": "integer" },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "merged_by": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "merged_at": { "type": ["date", "null"] },
- "closed_by": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "closed_at": { "type": ["date", "null"] },
- "created_at": { "type": "date" },
- "updated_at": { "type": "date" },
- "target_branch": { "type": "string" },
- "source_branch": { "type": "string" },
- "upvotes": { "type": "integer" },
- "downvotes": { "type": "integer" },
- "author": {
- "type": "object",
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "assignee": {
- "type": ["object", "null"],
- "properties": {
- "name": { "type": "string" },
- "username": { "type": "string" },
- "id": { "type": "integer" },
- "state": { "type": "string" },
- "avatar_url": { "type": "uri" },
- "web_url": { "type": "uri" }
- },
- "additionalProperties": false
- },
- "source_project_id": { "type": "integer" },
- "target_project_id": { "type": "integer" },
- "labels": {
- "type": "array",
- "items": {
- "type": "string"
- }
- },
- "work_in_progress": { "type": "boolean" },
- "milestone": {
- "type": ["object", "null"],
- "properties": {
- "id": { "type": "integer" },
- "iid": { "type": "integer" },
- "project_id": { "type": ["integer", "null"] },
- "group_id": { "type": ["integer", "null"] },
- "title": { "type": "string" },
- "description": { "type": ["string", "null"] },
- "state": { "type": "string" },
- "created_at": { "type": "date" },
- "updated_at": { "type": "date" },
- "due_date": { "type": "date" },
- "start_date": { "type": "date" }
- },
- "additionalProperties": false
- },
- "merge_when_pipeline_succeeds": { "type": "boolean" },
- "merge_status": { "type": "string" },
- "sha": { "type": "string" },
- "merge_commit_sha": { "type": ["string", "null"] },
- "user_notes_count": { "type": "integer" },
- "changes_count": { "type": "string" },
- "should_remove_source_branch": { "type": ["boolean", "null"] },
- "force_remove_source_branch": { "type": ["boolean", "null"] },
- "discussion_locked": { "type": ["boolean", "null"] },
- "web_url": { "type": "uri" },
- "squash": { "type": "boolean" },
- "time_stats": {
- "time_estimate": { "type": "integer" },
- "total_time_spent": { "type": "integer" },
- "human_time_estimate": { "type": ["string", "null"] },
- "human_total_time_spent": { "type": ["string", "null"] }
- },
- "allow_collaboration": { "type": ["boolean", "null"] },
- "allow_maintainer_to_push": { "type": ["boolean", "null"] }
- },
- "required": [
- "id", "iid", "project_id", "title", "description",
- "state", "created_at", "updated_at", "target_branch",
- "source_branch", "upvotes", "downvotes", "author",
- "assignee", "source_project_id", "target_project_id",
- "labels", "work_in_progress", "milestone", "merge_when_pipeline_succeeds",
- "merge_status", "sha", "merge_commit_sha", "user_notes_count",
- "should_remove_source_branch", "force_remove_source_branch",
- "web_url", "squash"
- ],
- "additionalProperties": false
+ "$ref": "./merge_request.json"
}
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json
index 56f86856dd4..a7207d2d991 100644
--- a/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/basic.json
@@ -13,6 +13,5 @@
"ref": { "type": "string" },
"status": { "type": "string" },
"web_url": { "type": "string" }
- },
- "additionalProperties": false
+ }
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json b/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json
new file mode 100644
index 00000000000..63e130d4055
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/pipeline/detail.json
@@ -0,0 +1,32 @@
+{
+ "type": "object",
+ "allOf": [
+ { "$ref": "basic.json" },
+ {
+ "properties": {
+ "before_sha": { "type": ["string", "null"] },
+ "tag": { "type": ["boolean"] },
+ "yaml_errors": { "type": ["string", "null"] },
+ "user": {
+ "anyOf": [
+ { "type": ["object", "null"] },
+ { "$ref": "../user/basic.json" }
+ ]
+ },
+ "created_at": { "type": ["date", "null"] },
+ "updated_at": { "type": ["date", "null"] },
+ "started_at": { "type": ["date", "null"] },
+ "finished_at": { "type": ["date", "null"] },
+ "committed_at": { "type": ["date", "null"] },
+ "duration": { "type": ["number", "null"] },
+ "coverage": { "type": ["string", "null"] },
+ "detailed_status": {
+ "oneOf": [
+ { "type": "null" },
+ { "$ref": "../../../status/ci_detailed_status.json" }
+ ]
+ }
+ }
+ }
+ ]
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release.json b/spec/fixtures/api/schemas/public_api/v4/release.json
index 6612c2a9911..6ea0781c1ed 100644
--- a/spec/fixtures/api/schemas/public_api/v4/release.json
+++ b/spec/fixtures/api/schemas/public_api/v4/release.json
@@ -1,12 +1,33 @@
{
"type": "object",
- "required" : [
- "tag_name",
- "description"
- ],
- "properties" : {
- "tag_name": { "type": ["string", "null"] },
- "description": { "type": "string" }
+ "required": ["name", "tag_name", "commit"],
+ "properties": {
+ "name": { "type": "string" },
+ "tag_name": { "type": "string" },
+ "description": { "type": "string" },
+ "description_html": { "type": "string" },
+ "created_at": { "type": "date" },
+ "commit": {
+ "oneOf": [{ "type": "null" }, { "$ref": "commit/basic.json" }]
+ },
+ "author": {
+ "oneOf": [{ "type": "null" }, { "$ref": "user/basic.json" }]
+ },
+ "assets": {
+ "required": ["count", "links", "sources"],
+ "properties": {
+ "count": { "type": "integer" },
+ "links": { "$ref": "../../release/links.json" },
+ "sources": {
+ "type": "array",
+ "items": {
+ "format": "zip",
+ "url": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
new file mode 100644
index 00000000000..e78398ad1d5
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/release/release_for_guest.json
@@ -0,0 +1,22 @@
+{
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "description_html": { "type": "string" },
+ "created_at": { "type": "date" },
+ "author": {
+ "oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
+ },
+ "assets": {
+ "required": ["count", "links"],
+ "properties": {
+ "count": { "type": "integer" },
+ "links": { "$ref": "../../../release/links.json" }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json b/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json
new file mode 100644
index 00000000000..c13966b28e9
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/release/releases_for_guest.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "release_for_guest.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json b/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json
new file mode 100644
index 00000000000..6612c2a9911
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/release/tag_release.json
@@ -0,0 +1,12 @@
+{
+ "type": "object",
+ "required" : [
+ "tag_name",
+ "description"
+ ],
+ "properties" : {
+ "tag_name": { "type": ["string", "null"] },
+ "description": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/releases.json b/spec/fixtures/api/schemas/public_api/v4/releases.json
new file mode 100644
index 00000000000..e26215707fe
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/releases.json
@@ -0,0 +1,4 @@
+{
+ "type": "array",
+ "items": { "$ref": "release.json" }
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/runner.json b/spec/fixtures/api/schemas/public_api/v4/runner.json
new file mode 100644
index 00000000000..d97d74a93f2
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/runner.json
@@ -0,0 +1,24 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "description",
+ "ip_address",
+ "active",
+ "is_shared",
+ "name",
+ "online",
+ "status"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "description": { "type": "string" },
+ "ip_address": { "type": "string" },
+ "active": { "type": "boolean" },
+ "is_shared": { "type": "boolean" },
+ "name": { "type": ["null", "string"] },
+ "online": { "type": "boolean" },
+ "status": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/tag.json b/spec/fixtures/api/schemas/public_api/v4/tag.json
index 10d4edb7ffb..5713ea1f526 100644
--- a/spec/fixtures/api/schemas/public_api/v4/tag.json
+++ b/spec/fixtures/api/schemas/public_api/v4/tag.json
@@ -14,7 +14,7 @@
"release": {
"oneOf": [
{ "type": "null" },
- { "$ref": "release.json" }
+ { "$ref": "release/tag_release.json" }
]
}
},
diff --git a/spec/fixtures/api/schemas/variable.json b/spec/fixtures/api/schemas/variable.json
index 6f6b044115b..305071a6b3f 100644
--- a/spec/fixtures/api/schemas/variable.json
+++ b/spec/fixtures/api/schemas/variable.json
@@ -4,12 +4,14 @@
"id",
"key",
"value",
+ "masked",
"protected"
],
"properties": {
"id": { "type": "integer" },
"key": { "type": "string" },
"value": { "type": "string" },
+ "masked": { "type": "boolean" },
"protected": { "type": "boolean" },
"environment_scope": { "type": "string", "optional": true }
},
diff --git a/spec/fixtures/blockquote_fence_after.md b/spec/fixtures/blockquote_fence_after.md
index 2652a842c0e..555905bf07e 100644
--- a/spec/fixtures/blockquote_fence_after.md
+++ b/spec/fixtures/blockquote_fence_after.md
@@ -18,10 +18,13 @@ Double `>>>` inside code block:
Blockquote outside code block:
+
> Quote
+
Code block inside blockquote:
+
> Quote
>
> ```
@@ -30,8 +33,10 @@ Code block inside blockquote:
>
> Quote
+
Single `>>>` inside code block inside blockquote:
+
> Quote
>
> ```
@@ -42,8 +47,10 @@ Single `>>>` inside code block inside blockquote:
>
> Quote
+
Double `>>>` inside code block inside blockquote:
+
> Quote
>
> ```
@@ -56,6 +63,7 @@ Double `>>>` inside code block inside blockquote:
>
> Quote
+
Single `>>>` inside HTML:
<pre>
@@ -76,10 +84,13 @@ Double `>>>` inside HTML:
Blockquote outside HTML:
+
> Quote
+
HTML inside blockquote:
+
> Quote
>
> <pre>
@@ -88,8 +99,10 @@ HTML inside blockquote:
>
> Quote
+
Single `>>>` inside HTML inside blockquote:
+
> Quote
>
> <pre>
@@ -100,8 +113,10 @@ Single `>>>` inside HTML inside blockquote:
>
> Quote
+
Double `>>>` inside HTML inside blockquote:
+
> Quote
>
> <pre>
@@ -113,3 +128,4 @@ Double `>>>` inside HTML inside blockquote:
> </pre>
>
> Quote
+
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
new file mode 100644
index 00000000000..638ecbcc11f
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml
@@ -0,0 +1,36 @@
+dashboard: 'Test Dashboard'
+priority: 1
+panel_groups:
+- group: Group A
+ priority: 1
+ panels:
+ - title: "Super Chart A1"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 1
+ metrics:
+ - id: metric_a1
+ query_range: 'query'
+ unit: unit
+ label: Legend Label
+ - title: "Super Chart A2"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 2
+ metrics:
+ - id: metric_a2
+ query_range: 'query'
+ label: Legend Label
+ unit: unit
+- group: Group B
+ priority: 10
+ panels:
+ - title: "Super Chart B"
+ type: "area-chart"
+ y_label: "y_label"
+ weight: 1
+ metrics:
+ - id: metric_b
+ query_range: 'query'
+ unit: unit
+ label: Legend Label
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
new file mode 100644
index 00000000000..1ee1205e29a
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/dashboard.json
@@ -0,0 +1,13 @@
+{
+ "type": "object",
+ "required": ["dashboard", "priority", "panel_groups"],
+ "properties": {
+ "dashboard": { "type": "string" },
+ "priority": { "type": "number" },
+ "panel_groups": {
+ "type": "array",
+ "items": { "$ref": "spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
new file mode 100644
index 00000000000..2d0af57ec2c
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "unit",
+ "label"
+ ],
+ "oneOf": [
+ { "required": ["query"] },
+ { "required": ["query_range"] }
+ ],
+ "properties": {
+ "id": { "type": "string" },
+ "query_range": { "type": "string" },
+ "query": { "type": "string" },
+ "unit": { "type": "string" },
+ "label": { "type": "string" },
+ "track": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
new file mode 100644
index 00000000000..d7a390adcdc
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panel_groups.json
@@ -0,0 +1,17 @@
+{
+ "type": "object",
+ "required": [
+ "group",
+ "priority",
+ "panels"
+ ],
+ "properties": {
+ "group": { "type": "string" },
+ "priority": { "type": "number" },
+ "panels": {
+ "type": "array",
+ "items": { "$ref": "panels.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
new file mode 100644
index 00000000000..1548daacd64
--- /dev/null
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "title",
+ "y_label",
+ "weight",
+ "metrics"
+ ],
+ "properties": {
+ "title": { "type": "string" },
+ "type": { "type": "string" },
+ "y_label": { "type": "string" },
+ "weight": { "type": "number" },
+ "metrics": {
+ "type": "array",
+ "items": { "$ref": "metrics.json" }
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/passphrase_x509_certificate.crt b/spec/fixtures/passphrase_x509_certificate.crt
new file mode 100644
index 00000000000..6973163b79e
--- /dev/null
+++ b/spec/fixtures/passphrase_x509_certificate.crt
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEpTCCAo0CAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
+MB4XDTE4MDMyMzE0MDIwOFoXDTE5MDMyMzE0MDIwOFowHTEbMBkGA1UEAwwSZ2l0
+bGFiLXBhc3NwaHJhc2VkMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
+zpsWHOewP/khfDsLUWxaRCinrBzVJm2C01bVahKVR3g/JD4vEH901Wod9Pvbh/9e
+PEfE+YZmgSUUopbL3JUheMnyW416F43HKE/fPW4+QeuIEceuhCXg20eOXmvnWWNM
+0hXZh4hq69rwvMPREC/LkZy/QkTDKhJNLNAqAQu2AJ3C7Yga8hFQYEhx1hpfGtwD
+z/Nf3efat9WN/d6yW9hfJ98NCmImTm5l9Pc0YPNWCAf96vsqsNHBrTkFy6CQwkhH
+K1ynVYuqnHYxSc4FPCT5SAleD9gR/xFBAHb7pPy4yGxMSEmiWaMjjZCVPsghj1jM
+Ej77MTDL3U9LeDfiILhvZ+EeQxqPiFwwG2eaIn3ZEs2Ujvw7Z2VpG9VMcPTnB4jK
+ot6qPM1YXnkGWQ6iT0DTPS3h7zg1xIJXI5N2sI6GXuKrXXwZ1wPqzFLKPv+xBjp8
+P6dih+EImfReFi9zIO1LqGMY+XmRcqodsb6jzsmBimJkqBtatJM7FuUUUN56wiaj
+q9+BWbm+ZdQ2lvqndMljjUjTh6pNERfGAJgkNuLn3X9hXVE0TSpmn0nOgaL5izP3
+7FWUt0PTyGgK2zq9SEhZmK2TKckLkKMk/ZBBBVM/nrnjs72IlbsqdcVoTnApytZr
+xVYTj1hV7QlAfaU3w/M534qXDiy8+HfX5ksWQMtSklECAwEAATANBgkqhkiG9w0B
+AQUFAAOCAgEAMMhzSRq9PqCpui74nwjhmn8Dm2ky7A+MmoXNtk70cS/HWrjzaacb
+B/rxsAUp7f0pj4QMMM0ETMFpbNs8+NPd2FRY0PfWE4yyDpvZO2Oj1HZKLHX72Gjn
+K5KB9DYlVsXhGPfuFWXpxGWF2Az9hDWnj58M3DOAps+6tHuAtudQUuwf5ENQZWwE
+ySpr7yoHm1ykgl0Tsb9ZHi9qLrWRRMNYXRT+gvwP1bba8j9jOtjO/xYiIskwMPLM
+W8SFmQxbg0Cvi8Q89PB6zoTNOhPQyoyeSlw9meeZJHAMK2zxeglEm8C4EQ+I9Y6/
+yylM5/Sc55TjWAvRFgbsq+OozgMvffk/Q2fzcGF44J9DEQ7nrhmJxJ+X4enLknR5
+Hw4+WhdYA+bwjx3YZBNTh9/YMgNPYwQhf5gtcZGTd6X4j6qZfJ6CXBmhkC1Cbfyl
+yM7B7i4JAqPWMeDP50pXCgyKlwgw1JuFW+xkbkYQAj7wtggQ6z1Vjb5W8R8kYn9q
+LXClVtThEeSV5KkVwNX21aFcUs8qeQ+zsgKqpEyM5oILQQ1gDSxLTtrr2KuN+WJN
+wM0acwD45X7gA/aZYpCGkIgHIBq0zIDP1s6IqeebFJjW8lWofhRxOEWomWdRweJG
+N7qQ1WCTQxAPGAkDI8QPjaspvnAhFKmpBG/mR5IXLFKDbttu7WNdYDo=
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/passphrase_x509_certificate_pk.key b/spec/fixtures/passphrase_x509_certificate_pk.key
new file mode 100644
index 00000000000..f9760dfe70e
--- /dev/null
+++ b/spec/fixtures/passphrase_x509_certificate_pk.key
@@ -0,0 +1,54 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: AES-128-CBC,79CCB506B0FD42A6F1BAE6D72E1CB20C
+
+EuZQOfgaO6LVCNytTHNJmbiq1rbum9xg6ohfBTVt7Cw4+8yLezWva/3sJQtnEk2P
+M2yEQYWIiCX+clPkRiRL8WLjRfLTNcYS6QxxuJdpOrowPrBYr4Aig8jBUUBI4VQf
+w1ZEUQd0mxQGnyzkKpsudFOntCtZbvbrBsIAQUNLcrKEFk3XW/BqE1Q/ja6WfWqX
+b6EKg6DoXi92V90O6sLDfpmTKZq3ThvVDFuWeJ2K/GVp2cs+MkBIBJ8XX+NT1nWg
+g+Ok+yaSI/N9ILX4XDgXunJGwcooI8PhHSjkDWRusi8vbo7RFqIKiSF+h6tIwktF
+Uss3JESKgXZCQ7upCnHSzK/aWFtwHtXxqOi7esqEZd+1sB0LY+XMnbaxweCMx2Kj
+czktKYvoXUs69Whln+yyXULtl5XhJ8lbvlbIG2FbZ9y+/hHOyBqZyeUyCnXDzv8/
+0U0iZwreP3XPVMsy578pIdcdL27q+r05j4yjrJfbX3T9xp2u3F9uVubCa4euEBwV
+yrFdsxJLKON8pFeDS49m5gHNsHmeZ0sUeTPZVGNXdabVetkOA0eAAGK4zAoqG79L
+hEN7cDenz+E4XHp8gMzwwMiVyU4FuAb6SXkfSodctmSTWVbzNBja0FBek3UXy+pn
+9qq7cIpe7NY5gzcbyoy9lSkyYVkAm8j6BIYtY1ZUAmtCklC2ADWARTjd7dI7aEbO
+QbXxNIq2+O/zMOXfougSPoDP8SLyLuE1p6SwfWV7Dwf119hn+mjWlGzAZDxxHhsR
+yYUQCUe0NIKzuUp3WYIx8xIb7/WFwit/JaFaxurjBnhkkEviBn+TgXiuFBO3tv/d
+URpZ39rH0mrDsR61pCiIcoNVkQkynHcAFPd5VtaeSJPvZP280uOCPPS31cr6/0LB
+1JX3lZoWWCuA+JQjxtZDaDTcvEUbfOQ2rexQQo4uylNkBF9F5WOdQBkKG/AfqBq8
+S/TdubYzvpcKhFAlXsI67JdbxGlU4HCsxOLwWzSUYclN4W3l7s7KZ5zxt+MU03Uf
+vara9uuZHiKUjZohjXeqcXTc+UyC8VH1dF19M3Cj9RNrwl2xEDUMtIiALBjbGp1E
+pu2nPj9NhWf9Vw5MtSszutesxXba2nPmvvGvvZ7N3h/k4NsKL7JdENF7XqkI0D2K
+jpO1t6d3cazS1VpMWLZS45kWaM3Y07tVR3V+4Iv9Vo1e9H2u/Z5U4YeJ44sgMsct
+dBOAhHdUAI5+P+ocLXiCKo+EcS0cKvz+CC4ux0vvcF3JrTqZJN1U/JxRka2EyJ1B
+2Xtu3DF36XpBJcs+MJHjJ+kUn6DHYoYxZa+bB8LX6+FQ+G7ue+Dx/RsGlP7if1nq
+DAaM6kZg7/FbFzOZyl5xhwAJMxfgNNU7nSbk9lrvQ4mdwgFjvgGu3jlER4+TcleE
+4svXInxp1zK6ES44tI9fXkhPaFkafxAL7eUSyjjEwMC06h+FtqK3mmoKLo5NrGJE
+zVl69r2WdoSQEylVN1Kbp+U4YbfncInLJqBq2q5w9ASL/8Rhe8b52q6PuVX/bjoz
+0pkSu+At4jVbAhRpER5NGlzG884IaqqvBvMYR5zFJeRroIijyUyH0KslK37/sXRk
+ty0yKrkm31De9gDa3+XlgAVDAgbEQmGVwVVcV0IYYJbjIf36lUdGh4+3krwxolr/
+vZct5Z7QxfJlBtdOstjz5U9o05yOhjoNrPZJXuKMmWOQjSwr7rRSdqmAABF9IrBf
+Pa/ChF1y5j3gJESAFMyiea3kvLq1EbZRaKoybsQE2ctBQ8EQjzUz+OOxVO6GJ4W9
+XHyfcviFrpsVcJEpXQlEtGtKdfKLp48cytob1Fu1JOYPDCrafUQINCZP4H3Nt892
+zZiTmdwux7pbgf4KbONImN5XkpvdCGjQHSkYMmm5ETRK8s7Fmvt2aBPtlyXxJDOq
+iJUqwDV5HZXOnQVE/v/yESKgo2Cb8BWqPZ4/8Ubgu/OADYyv/dtjQel8QQ2FMhO4
+2tnwWbBBJk8VpR/vjFHkGSnj+JJfW/vUVQ+06D3wHYhNp7mh4M+37AngwzGCp7k+
+9aFwb2FBGghArB03E4lIO/959T0cX95WZ6tZtLLEsf3+ug7PPOSswCqsoPsXzFJH
+MgXVGKFXccNSsWol7VvrX/uja7LC1OE+pZNXxCRzSs4aljJBpvQ6Mty0lk2yBC0R
+MdujMoZH9PG9U6stwFd+P17tlGrQdRD3H2uimn82Ck+j2l0z0pzN0JB2WBYEyK0O
+1MC36wLICWjgIPLPOxDEEBeZPbc24DCcYfs/F/hSCHv/XTJzVVILCX11ShGPSXlI
+FL9qyq6jTNh/pVz6NiN/WhUPBFfOSzLRDyU0MRsSHM8b/HPpf3NOI3Ywmmj65c2k
+2kle1F2M5ZTL+XvLS61qLJ/8AgXWvDHP3xWuKGG/pM40CRTUkRW6NAokMr2/pEFw
+IHTE2+84dOKnUIEczzMY3aqzNmYDCmhOY0jD/Ieb4hy9tN+1lbQ/msYMIJ1w7CFR
+38yB/UbDD90NcuDhjrMbzVUv1At2rW7GM9lSbxGOlYDmtMNEL63md1pQ724v4gSE
+mzoFcMkqdh+hjFvv11o4H32lF3mPYcXuL+po76tqxGOiUrLKe/ZqkT5XAclYV/7H
+k3Me++PCh4ZqXBRPvR8Xr90NETtiFCkBQXLdhNWXrRe2v0EbSX+cYAWk68FQKCHa
+HKTz9T7wAvB6QWBXFhH9iCP8rnQLCEhLEhdrt+4v2KFkIVzBgOlMoHsZsMp0sBeq
+c5ZVbJdiKik3P/8ZQTn4jmOnQXCEyWx+LU4acks8Aho4lqq9yKq2DZpwbIRED47E
+r7R/NUevhqqzEHZ2SGD6EDqRN+bHJEi64vq0ryaEielusYXZqlnFXDHJcfLCmR5X
+3bj5pCwQF4ScTukrGQB/c4henG4vlF4CaD0CIIK3W6tH+AoDohYJts6YK49LGxmK
+yXiyKNak8zHYBBoRvd2avRHyGuR5yC9KrN8cbC/kZqMDvAyM65pIK+U7exJwYJhv
+ezCcbiH3bK3anpiRpdeNOot2ba/Y+/ks+DRC+xs4QDIhrmSEBCsLv1JbcWjtHSaG
+lm+1DSVduUk/kN+fBnlfif+TQV9AP3/wb8ekk8jjKXsL7H1tJKHsLLIIvrgrpxjw
+-----END RSA PRIVATE KEY-----
diff --git a/spec/fixtures/phabricator_responses/auth_failed.json b/spec/fixtures/phabricator_responses/auth_failed.json
new file mode 100644
index 00000000000..50e57c0ba49
--- /dev/null
+++ b/spec/fixtures/phabricator_responses/auth_failed.json
@@ -0,0 +1 @@
+{"result":null,"error_code":"ERR-INVALID-AUTH","error_info":"API token \"api-token\" has the wrong length. API tokens should be 32 characters long."}
diff --git a/spec/fixtures/phabricator_responses/maniphest.search.json b/spec/fixtures/phabricator_responses/maniphest.search.json
new file mode 100644
index 00000000000..6a965007d0c
--- /dev/null
+++ b/spec/fixtures/phabricator_responses/maniphest.search.json
@@ -0,0 +1,98 @@
+{
+ "result": {
+ "data": [
+ {
+ "id": 283,
+ "type": "TASK",
+ "phid": "PHID-TASK-fswfs3wkowjb6cyyxtyx",
+ "fields": {
+ "name": "Things are slow",
+ "description": {
+ "raw": "Things are slow but should be fast!"
+ },
+ "authorPHID": "PHID-USER-nrtht5wijwbxquens3qr",
+ "ownerPHID": "PHID-USER-nrtht5wijwbxquens3qr",
+ "status": {
+ "value": "resolved",
+ "name": "Resolved",
+ "color": null
+ },
+ "priority": {
+ "value": 100,
+ "subpriority": 8589934592,
+ "name": "Super urgent",
+ "color": "pink"
+ },
+ "points": null,
+ "subtype": "default",
+ "closerPHID": "PHID-USER-nrtht5wijwbxquens3qr",
+ "dateClosed": 1374657042,
+ "spacePHID": null,
+ "dateCreated": 1374616241,
+ "dateModified": 1374657044,
+ "policy": {
+ "view": "users",
+ "interact": "users",
+ "edit": "users"
+ },
+ "custom.field-1": null,
+ "custom.field-2": null,
+ "custom.field-3": null
+ },
+ "attachments": {}
+ },
+ {
+ "id": 284,
+ "type": "TASK",
+ "phid": "PHID-TASK-5f73nyq5sjeh4cbmcsnb",
+ "fields": {
+ "name": "Things are broken",
+ "description": {
+ "raw": "Things are broken and should be fixed"
+ },
+ "authorPHID": "PHID-USER-nrtht5wijwbxquens3qr",
+ "ownerPHID": "PHID-USER-h425fsrixz4gjxiyr7ot",
+ "status": {
+ "value": "resolved",
+ "name": "Resolved",
+ "color": null
+ },
+ "priority": {
+ "value": 100,
+ "subpriority": 8589803520,
+ "name": "Super urgent",
+ "color": "pink"
+ },
+ "points": null,
+ "subtype": "default",
+ "closerPHID": "PHID-USER-h425fsrixz4gjxiyr7ot",
+ "dateClosed": 1375049556,
+ "spacePHID": null,
+ "dateCreated": 1374616578,
+ "dateModified": 1375049556,
+ "policy": {
+ "view": "users",
+ "interact": "users",
+ "edit": "users"
+ },
+ "custom.field-1": null,
+ "custom.field-2": null,
+ "custom.field-3": null
+ },
+ "attachments": {}
+ }
+ ],
+ "maps": {},
+ "query": {
+ "queryKey": null
+ },
+ "cursor": {
+ "limit": "2",
+ "after": "284",
+ "before": null,
+ "order": null
+ }
+ },
+ "error_code": null,
+ "error_info": null
+}
diff --git a/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json
new file mode 100644
index 00000000000..1e62d020026
--- /dev/null
+++ b/spec/fixtures/security-reports/dependency_list/gl-dependency-scanning-report.json
@@ -0,0 +1,422 @@
+{
+ "version": "2.1",
+ "vulnerabilities": [
+ {
+ "category": "dependency_scanning",
+ "name": "Vulnerabilities in libxml2",
+ "message": "Vulnerabilities in libxml2 in nokogiri",
+ "description": " The version of libxml2 packaged with Nokogiri contains several vulnerabilities.\r\n Nokogiri has mitigated these issues by upgrading to libxml 2.9.5.\r\n\r\n It was discovered that a type confusion error existed in libxml2. An\r\n attacker could use this to specially construct XML data that\r\n could cause a denial of service or possibly execute arbitrary\r\n code. (CVE-2017-0663)\r\n\r\n It was discovered that libxml2 did not properly validate parsed entity\r\n references. An attacker could use this to specially construct XML\r\n data that could expose sensitive information. (CVE-2017-7375)\r\n\r\n It was discovered that a buffer overflow existed in libxml2 when\r\n handling HTTP redirects. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-7376)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overflow in\r\n libxml2 when handling elements. An attacker could use this to specially\r\n construct XML data that could cause a denial of service or possibly\r\n execute arbitrary code. (CVE-2017-9047)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered a buffer overread\r\n in libxml2 when handling elements. An attacker could use this\r\n to specially construct XML data that could cause a denial of\r\n service. (CVE-2017-9048)\r\n\r\n Marcel Böhme and Van-Thuan Pham discovered multiple buffer overreads\r\n in libxml2 when handling parameter-entity references. An attacker\r\n could use these to specially construct XML data that could cause a\r\n denial of service. (CVE-2017-9049, CVE-2017-9050)",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:06565b64-486d-4326-b906-890d9915804d",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-06565b64-486d-4326-b906-890d9915804d",
+ "value": "06565b64-486d-4326-b906-890d9915804d",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "usn",
+ "name": "USN-3424-1",
+ "value": "USN-3424-1",
+ "url": "https://usn.ubuntu.com/3424-1/"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1673"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Infinite recursion in parameter entities",
+ "message": "Infinite recursion in parameter entities in nokogiri",
+ "description": "libxml2 incorrectly handles certain parameter entities. An attacker can leverage this with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "value": "6a0d56f6-2441-492a-9b14-edb95ac31919",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-16932",
+ "value": "CVE-2017-16932",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-16932"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1714"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-16932.html"
+ },
+ {
+ "url": "https://usn.ubuntu.com/usn/usn-3504-1/"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Denial of Service",
+ "message": "Denial of Service in nokogiri",
+ "description": "libxml2 incorrectly handles certain files. An attacker can use this issue with specially constructed XML data to cause libxml2 to consume resources, leading to a denial of service.\r\n\r\n",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:78658378-bd8f-4d79-81c8-07c419302426",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-78658378-bd8f-4d79-81c8-07c419302426",
+ "value": "78658378-bd8f-4d79-81c8-07c419302426",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-15412",
+ "value": "CVE-2017-15412",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-15412"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1714"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/2017/CVE-2017-15412.html"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Bypass of a protection mechanism in libxslt",
+ "message": "Bypass of a protection mechanism in libxslt in nokogiri",
+ "description": "libxslt through 1.1.33 allows bypass of a protection mechanism because callers of xsltCheckRead and xsltCheckWrite permit access even upon receiving a -1 error code. xsltCheckRead can return -1 for a crafted URL that is not actually invalid and is subsequently loaded. Vendored version of libxslt has been patched to remediate this vulnerability. Note that this patch is not yet (as of 2019-04-22) in an upstream release of libxslt.",
+ "cve": "rails/Gemfile.lock:nokogiri:gemnasium:1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest version if using vendored version of libxslt OR update the system library libxslt to a fixed version",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "rails/Gemfile.lock",
+ "dependency": {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "value": "1a2e2e6e-67ba-4142-bfa1-3391f5416e4c",
+ "url": "https://deps.sec.gitlab.com/packages/gem/nokogiri/versions/1.8.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2019-11068",
+ "value": "CVE-2019-11068",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11068"
+ },
+ {
+ "url": "https://github.com/sparklemotion/nokogiri/issues/1892"
+ },
+ {
+ "url": "https://people.canonical.com/~ubuntu-security/cve/CVE-2019-11068"
+ },
+ {
+ "url": "https://security-tracker.debian.org/tracker/CVE-2019-11068"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Regular Expression Denial of Service",
+ "message": "Regular Expression Denial of Service in debug",
+ "description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
+ "cve": "yarn/yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest versions.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "yarn/yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "debug"
+ },
+ "version": "1.0.5"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "value": "37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "url": "https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/visionmedia/debug/issues/501"
+ },
+ {
+ "url": "https://github.com/visionmedia/debug/pull/504"
+ },
+ {
+ "url": "https://nodesecurity.io/advisories/534"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Authentication bypass via incorrect DOM traversal and canonicalization",
+ "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
+ "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
+ "cve": "yarn/yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98",
+ "severity": "Unknown",
+ "solution": "Upgrade to fixed version.\r\n",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "yarn/yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
+ "value": "9952e574-7b5b-46fa-a270-aeb694198a98",
+ "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-11429",
+ "value": "CVE-2017-11429",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
+ },
+ {
+ "url": "https://github.com/Clever/saml2/issues/127"
+ },
+ {
+ "url": "https://www.kb.cert.org/vuls/id/475445"
+ }
+ ]
+ }
+ ],
+ "remediations": [],
+ "dependency_files": [
+ {
+ "path": "rails/Gemfile.lock",
+ "package_manager": "bundler",
+ "dependencies": [
+ {
+ "package": {
+ "name": "mini_portile2"
+ },
+ "version": "2.2.0"
+ },
+ {
+ "package": {
+ "name": "nokogiri"
+ },
+ "version": "1.8.0"
+ }
+ ]
+ },
+ {
+ "path": "yarn/yarn.lock",
+ "package_manager": "yarn",
+ "dependencies": [
+ {
+ "package": {
+ "name": "async"
+ },
+ "version": "0.2.10"
+ },
+ {
+ "package": {
+ "name": "async"
+ },
+ "version": "1.5.2"
+ },
+ {
+ "package": {
+ "name": "debug"
+ },
+ "version": "1.0.5"
+ },
+ {
+ "package": {
+ "name": "ejs"
+ },
+ "version": "0.8.8"
+ },
+ {
+ "package": {
+ "name": "ms"
+ },
+ "version": "2.0.0"
+ },
+ {
+ "package": {
+ "name": "node-forge"
+ },
+ "version": "0.2.24"
+ },
+ {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ },
+ {
+ "package": {
+ "name": "sax"
+ },
+ "version": "1.2.4"
+ },
+ {
+ "package": {
+ "name": "underscore"
+ },
+ "version": "1.9.1"
+ },
+ {
+ "package": {
+ "name": "underscore"
+ },
+ "version": "1.6.0"
+ },
+ {
+ "package": {
+ "name": "xml-crypto"
+ },
+ "version": "0.8.5"
+ },
+ {
+ "package": {
+ "name": "xml-encryption"
+ },
+ "version": "0.7.4"
+ },
+ {
+ "package": {
+ "name": "xml2js"
+ },
+ "version": "0.4.19"
+ },
+ {
+ "package": {
+ "name": "xmlbuilder"
+ },
+ "version": "2.1.0"
+ },
+ {
+ "package": {
+ "name": "xmlbuilder"
+ },
+ "version": "9.0.7"
+ },
+ {
+ "package": {
+ "name": "xmldom"
+ },
+ "version": "0.1.19"
+ },
+ {
+ "package": {
+ "name": "xmldom"
+ },
+ "version": "0.1.27"
+ },
+ {
+ "package": {
+ "name": "xpath.js"
+ },
+ "version": "1.1.0"
+ },
+ {
+ "package": {
+ "name": "xpath"
+ },
+ "version": "0.0.5"
+ }
+ ]
+ }
+ ]
+}
diff --git a/spec/fixtures/security-reports/master/gl-dast-report.json b/spec/fixtures/security-reports/master/gl-dast-report.json
index 3a308bf047e..df459d9419d 100644
--- a/spec/fixtures/security-reports/master/gl-dast-report.json
+++ b/spec/fixtures/security-reports/master/gl-dast-report.json
@@ -1,40 +1,42 @@
{
- "site": {
- "alerts": [
- {
- "sourceid": "3",
- "wascid": "15",
- "cweid": "16",
- "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
- "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
- "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
- "count": "2",
- "pluginid": "10021",
- "alert": "X-Content-Type-Options Header Missing",
- "name": "X-Content-Type-Options Header Missing",
- "riskcode": "1",
- "confidence": "2",
- "riskdesc": "Low (Medium)",
- "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
- "instances": [
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
- {
- "param": "X-Content-Type-Options",
- "method": "GET",
- "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
- }
- ]
- }
- ],
- "@ssl": "false",
- "@port": "80",
- "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
- "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
- },
+ "site": [
+ {
+ "alerts": [
+ {
+ "sourceid": "3",
+ "wascid": "15",
+ "cweid": "16",
+ "reference": "<p>http://msdn.microsoft.com/en-us/library/ie/gg622941%28v=vs.85%29.aspx</p><p>https://www.owasp.org/index.php/List_of_useful_HTTP_headers</p>",
+ "otherinfo": "<p>This issue still applies to error type pages (401, 403, 500, etc) as those pages are often still affected by injection issues, in which case there is still concern for browsers sniffing pages away from their actual content type.</p><p>At \"High\" threshold this scanner will not alert on client or server error responses.</p>",
+ "solution": "<p>Ensure that the application/web server sets the Content-Type header appropriately, and that it sets the X-Content-Type-Options header to 'nosniff' for all web pages.</p><p>If possible, ensure that the end user uses a standards-compliant and modern web browser that does not perform MIME-sniffing at all, or that can be directed by the web application/web server to not perform MIME-sniffing.</p>",
+ "count": "2",
+ "pluginid": "10021",
+ "alert": "X-Content-Type-Options Header Missing",
+ "name": "X-Content-Type-Options Header Missing",
+ "riskcode": "1",
+ "confidence": "2",
+ "riskdesc": "Low (Medium)",
+ "desc": "<p>The Anti-MIME-Sniffing header X-Content-Type-Options was not set to 'nosniff'. This allows older versions of Internet Explorer and Chrome to perform MIME-sniffing on the response body, potentially causing the response body to be interpreted and displayed as a content type other than the declared content type. Current (early 2014) and legacy versions of Firefox will use the declared content type (if one is set), rather than performing MIME-sniffing.</p>",
+ "instances": [
+ {
+ "param": "X-Content-Type-Options",
+ "method": "GET",
+ "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
+ },
+ {
+ "param": "X-Content-Type-Options",
+ "method": "GET",
+ "uri": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io/"
+ }
+ ]
+ }
+ ],
+ "@ssl": "false",
+ "@port": "80",
+ "@host": "bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io",
+ "@name": "http://bikebilly-spring-auto-devops-review-feature-br-3y2gpb.35.192.176.43.xip.io"
+ }
+ ],
"@generated": "Fri, 13 Apr 2018 09:22:01",
"@version": "2.7.0"
}
diff --git a/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json b/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json
new file mode 100644
index 00000000000..c96e831b027
--- /dev/null
+++ b/spec/fixtures/security-reports/remediations/gl-dependency-scanning-report.json
@@ -0,0 +1,104 @@
+{
+ "version": "2.0",
+ "vulnerabilities": [
+ {
+ "category": "dependency_scanning",
+ "name": "Regular Expression Denial of Service",
+ "message": "Regular Expression Denial of Service in debug",
+ "description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the `o` formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.",
+ "cve": "yarn.lock:debug:gemnasium:37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "severity": "Unknown",
+ "solution": "Upgrade to latest versions.",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "debug"
+ },
+ "version": "1.0.5"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "value": "37283ed4-0380-40d7-ada7-2d994afcc62a",
+ "url": "https://deps.sec.gitlab.com/packages/npm/debug/versions/1.0.5/advisories"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://nodesecurity.io/advisories/534"
+ },
+ {
+ "url": "https://github.com/visionmedia/debug/issues/501"
+ },
+ {
+ "url": "https://github.com/visionmedia/debug/pull/504"
+ }
+ ]
+ },
+ {
+ "category": "dependency_scanning",
+ "name": "Authentication bypass via incorrect DOM traversal and canonicalization",
+ "message": "Authentication bypass via incorrect DOM traversal and canonicalization in saml2-js",
+ "description": "Some XML DOM traversal and canonicalization APIs may be inconsistent in handling of comments within XML nodes. Incorrect use of these APIs by some SAML libraries results in incorrect parsing of the inner text of XML nodes such that any inner text after the comment is lost prior to cryptographically signing the SAML message. Text after the comment therefore has no impact on the signature on the SAML message.\r\n\r\nA remote attacker can modify SAML content for a SAML service provider without invalidating the cryptographic signature, which may allow attackers to bypass primary authentication for the affected SAML service provider.",
+ "cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98",
+ "severity": "Unknown",
+ "solution": "Upgrade to fixed version.\r\n",
+ "scanner": {
+ "id": "gemnasium",
+ "name": "Gemnasium"
+ },
+ "location": {
+ "file": "yarn.lock",
+ "dependency": {
+ "package": {
+ "name": "saml2-js"
+ },
+ "version": "1.5.0"
+ }
+ },
+ "identifiers": [
+ {
+ "type": "gemnasium",
+ "name": "Gemnasium-9952e574-7b5b-46fa-a270-aeb694198a98",
+ "value": "9952e574-7b5b-46fa-a270-aeb694198a98",
+ "url": "https://deps.sec.gitlab.com/packages/npm/saml2-js/versions/1.5.0/advisories"
+ },
+ {
+ "type": "cve",
+ "name": "CVE-2017-11429",
+ "value": "CVE-2017-11429",
+ "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11429"
+ }
+ ],
+ "links": [
+ {
+ "url": "https://github.com/Clever/saml2/commit/3546cb61fd541f219abda364c5b919633609ef3d#diff-af730f9f738de1c9ad87596df3f6de84R279"
+ },
+ {
+ "url": "https://github.com/Clever/saml2/issues/127"
+ },
+ {
+ "url": "https://www.kb.cert.org/vuls/id/475445"
+ }
+ ]
+ }
+ ],
+ "remediations": [
+ {
+ "fixes": [
+ {
+ "cve": "yarn.lock:saml2-js:gemnasium:9952e574-7b5b-46fa-a270-aeb694198a98"
+ }
+ ],
+ "summary": "Upgrade saml2-js",
+ "diff": "ZGlmZiAtLWdpdCBhL3lhcm4ubG9jayBiL3lhcm4ubG9jawppbmRleCAwZWNjOTJmLi43ZmE0NTU0IDEwMDY0NAotLS0gYS95YXJuLmxvY2sKKysrIGIveWFybi5sb2NrCkBAIC0yLDEwMyArMiwxMjQgQEAKICMgeWFybiBsb2NrZmlsZSB2MQogCiAKLWFzeW5jQH4wLjIuNzoKLSAgdmVyc2lvbiAiMC4yLjEwIgotICByZXNvbHZlZCAiaHR0cDovL3JlZ2lzdHJ5Lm5wbWpzLm9yZy9hc3luYy8tL2FzeW5jLTAuMi4xMC50Z3ojYjZiYmUwYjA2NzRiOWQ3MTk3MDhjYTM4ZGU4YzIzN2NiNTI2YzNkMSIKLQotYXN5bmNAfjEuNS4yOgotICB2ZXJzaW9uICIxLjUuMiIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcvYXN5bmMvLS9hc3luYy0xLjUuMi50Z3ojZWM2YTYxYWU1NjQ4MGMwYzNjYjI0MWM5NTYxOGUyMDg5MmY5NjcyYSIKK2FzeW5jQF4yLjEuNSwgYXN5bmNAXjIuNS4wOgorICB2ZXJzaW9uICIyLjYuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vYXN5bmMvLS9hc3luYy0yLjYuMS50Z3ojYjI0NWEyM2NhNzE5MzAwNDRlYzUzZmE0NmFhMDBhM2U4N2M2YTYxMCIKKyAgaW50ZWdyaXR5IHNoYTUxMi1mTkVpTDIrQVp0NkFsQXcvMjlDcjBVRGU0c1JBSENwRUhoNTRXTXorQmI3UWZOY0Z3NGgzbG9vZnlKcExlUXM0WXg3eXVxdS8yZExnTTVoS09zNkhsUT09CisgIGRlcGVuZGVuY2llczoKKyAgICBsb2Rhc2ggIl40LjE3LjEwIgogCi1kZWJ1Z0BeMS4wLjQ6Ci0gIHZlcnNpb24gIjEuMC41IgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9kZWJ1Zy8tL2RlYnVnLTEuMC41LnRneiNmNzI0MTIxNzQzMGY5OWRlYzRjMmI0NzNlYWI5MjIyOGU4NzRjMmFjIgorZGVidWdAXjIuNi4wOgorICB2ZXJzaW9uICIyLjYuOSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vZGVidWcvLS9kZWJ1Zy0yLjYuOS50Z3ojNWQxMjg1MTVkZjEzNGZmMzI3ZTkwYTRjOTNmNGUwNzdhNTM2MzQxZiIKKyAgaW50ZWdyaXR5IHNoYTUxMi1iQzdFbHJkSmFKblBiQVArMUVvdFl2cVpzYjNlY2w1d2k2QmZpNkJKVFVjTm93cDZjdnNwZzBqWHpuUlRLRGptL0U3QWRnRkJWZUFQVk1OY0tHc0hNQT09CiAgIGRlcGVuZGVuY2llczoKICAgICBtcyAiMi4wLjAiCiAKLWVqc0B+MC44LjM6Ci0gIHZlcnNpb24gIjAuOC44IgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9lanMvLS9lanMtMC44LjgudGd6I2ZmZGM1NmRjYzM1ZDAyOTI2ZGQ1MGFkMTM0MzliYmM1NDA2MWQ1OTgiCitlanNAXjIuNS42OgorICB2ZXJzaW9uICIyLjYuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vZWpzLy0vZWpzLTIuNi4xLnRneiM0OThlYzBkNDk1NjU1YWJjNmYyM2NkNjE4NjhkOTI2NDY0MDcxYWEwIgorICBpbnRlZ3JpdHkgc2hhNTEyLTB4eTRBL3R3ZnJSQ25raGZrOEVyRGk1RHFkQXNBcWVHeGh0NHhrQ1Vyc3ZoaGJRTnM3RSs0alYwQ043K05LSVkwYUhFNzIrWHZxdEJJWHpEMzFaYlhRPT0KKworbG9kYXNoLW5vZGVAfjIuNC4xOgorICB2ZXJzaW9uICIyLjQuMSIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vbG9kYXNoLW5vZGUvLS9sb2Rhc2gtbm9kZS0yLjQuMS50Z3ojZWE4MmY3YjEwMGM3MzNkMWE0MmFmNzY4MDFlNTA2MTA1ZTJhODBlYyIKKyAgaW50ZWdyaXR5IHNoYTEtNm9MM3NRREhNOUdrS3Zkb0FlVUdFRjRxZ093PQorCitsb2Rhc2hAXjQuMTcuMTA6CisgIHZlcnNpb24gIjQuMTcuMTEiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL2xvZGFzaC8tL2xvZGFzaC00LjE3LjExLnRneiNiMzllYTYyMjllZjYwN2VjZDg5ZTJjOGRmMTI1MzY4OTFjYWM5YjhkIgorICBpbnRlZ3JpdHkgc2hhNTEyLWNRS2g4aWdvNVFVaFo3bGczOERZV0F4TXZqU0FLRzBBOHdHU1ZpbVAwN1NJVUVLMlVPK2FyU1JLYlJaV3RlbE10TjVWMEhrd2g1cnlPdG8vU3NoWUlnPT0KIAogbXNAMi4wLjA6CiAgIHZlcnNpb24gIjIuMC4wIgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9tcy8tL21zLTIuMC4wLnRneiM1NjA4YWVhZGZjMDBiZTZjMjkwMWRmNWY5ODYxNzg4ZGUwZDU5N2M4IgorICBpbnRlZ3JpdHkgc2hhMS1WZ2l1cmZ3QXZtd3BBZDlmbUdGNGplRFZsOGc9CiAKLW5vZGUtZm9yZ2VAMC4yLjI0OgotICB2ZXJzaW9uICIwLjIuMjQiCi0gIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL25vZGUtZm9yZ2UvLS9ub2RlLWZvcmdlLTAuMi4yNC50Z3ojZmE2Zjg0NmY0MmZhOTNmNjNhMGEzMGM5ZmJmZjdiNGUxMzBlMDg1OCIKK25vZGUtZm9yZ2VAXjAuNy4wOgorICB2ZXJzaW9uICIwLjcuNiIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20vbm9kZS1mb3JnZS8tL25vZGUtZm9yZ2UtMC43LjYudGd6I2ZkZjNiNDE4YWVlMWY5NGYwZWY2NDJjZDYzNDg2Yzc3Y2E5NzI0YWMiCisgIGludGVncml0eSBzaGE1MTItc29sMzBMVXB6MWpRRkJqT0t3Ymp4aWppRTNiNnBqZDc0WXdmRDBmSk9LUGpGK2ZPTktiMllnOHJZZ1M2K2JLNlZEbCsvd2ZyNElZcEM3akR6TFVJZnc9PQogCiBzYW1sMi1qc0BeMS41LjA6Ci0gIHZlcnNpb24gIjEuNS4wIgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9zYW1sMi1qcy8tL3NhbWwyLWpzLTEuNS4wLnRneiNjMGQyMjY4YTE3OWU3MzI5ZDI5ZWIyNWFhODJkZjU1MDM3NzRiMGQ5IgorICB2ZXJzaW9uICIxLjEyLjQiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3NhbWwyLWpzLy0vc2FtbDItanMtMS4xMi40LnRneiNjMjg4ZjIwYmRhNmQyYjkxMDczYjE2Yzk0ZWE3MmYyMjM0OWFjM2IzIgorICBpbnRlZ3JpdHkgc2hhMS13b2p5QzlwdEs1RUhPeGJKVHFjdklqU2F3N009CiAgIGRlcGVuZGVuY2llczoKLSAgICBhc3luYyAifjEuNS4yIgotICAgIGRlYnVnICJeMS4wLjQiCi0gICAgdW5kZXJzY29yZSAifjEuNi4wIgotICAgIHhtbC1jcnlwdG8gIl4wLjguMSIKLSAgICB4bWwtZW5jcnlwdGlvbiAifjAuNy40IgotICAgIHhtbDJqcyAifjAuNC4xIgotICAgIHhtbGJ1aWxkZXIgIn4yLjEuMCIKLSAgICB4bWxkb20gIn4wLjEuMTkiCisgICAgYXN5bmMgIl4yLjUuMCIKKyAgICBkZWJ1ZyAiXjIuNi4wIgorICAgIHVuZGVyc2NvcmUgIl4xLjguMCIKKyAgICB4bWwtY3J5cHRvICJeMC4xMC4wIgorICAgIHhtbC1lbmNyeXB0aW9uICJeMC4xMS4wIgorICAgIHhtbDJqcyAiXjAuNC4wIgorICAgIHhtbGJ1aWxkZXIgIn4yLjIuMCIKKyAgICB4bWxkb20gIl4wLjEuMCIKIAogc2F4QD49MC42LjA6CiAgIHZlcnNpb24gIjEuMi40IgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS9zYXgvLS9zYXgtMS4yLjQudGd6IzI4MTYyMzRlMjM3OGJkZGM0ZTUzNTRmYWI1Y2FhODk1ZGY3MTAwZDkiCisgIGludGVncml0eSBzaGE1MTItTnFWRHY5VHBBTlVqRm0wTjh1TTVHeEwzNlVnS2k5L2F0WncreDdZRm5ROGNrd0ZHS3JsNHhYNHlXdHJleTNVSm01blAxa1VibllnTG9wcVdOU1JoV3c9PQogCi11bmRlcnNjb3JlQD49MS41Lng6Cit1bmRlcnNjb3JlQF4xLjguMDoKICAgdmVyc2lvbiAiMS45LjEiCiAgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3VuZGVyc2NvcmUvLS91bmRlcnNjb3JlLTEuOS4xLnRneiMwNmRjZTM0YTBlNjhhN2JhYmMyOWIzNjViOGU3NGI4OTI1MjAzOTYxIgorICBpbnRlZ3JpdHkgc2hhNTEyLTUvNGV0bkNrZDljOGd3Z293aTUvb20vbVlPNWFqQ2FPZ2R6ai9vVyswZVFWOVd4S0JEWnc1K3ljbUttZWFUWGpJblMvVzBCenBHTG8yeFIyYUJ3WmRnPT0KIAotdW5kZXJzY29yZUB+MS42LjA6Ci0gIHZlcnNpb24gIjEuNi4wIgotICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS91bmRlcnNjb3JlLy0vdW5kZXJzY29yZS0xLjYuMC50Z3ojOGIzOGIxMGNhY2RlZjYzMzM3YjhiMjRlNGZmODZkNDVhZWE1MjlhOCIKLQoteG1sLWNyeXB0b0BeMC44LjE6Ci0gIHZlcnNpb24gIjAuOC41IgotICByZXNvbHZlZCAiaHR0cDovL3JlZ2lzdHJ5Lm5wbWpzLm9yZy94bWwtY3J5cHRvLy0veG1sLWNyeXB0by0wLjguNS50Z3ojMmJiY2ZiM2ViMzNmM2E4MmEyMThiODIyYmY2NzJiNmIxYzIwZTUzOCIKK3htbC1jcnlwdG9AXjAuMTAuMDoKKyAgdmVyc2lvbiAiMC4xMC4xIgorICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94bWwtY3J5cHRvLy0veG1sLWNyeXB0by0wLjEwLjEudGd6I2Y4MzJmNzRjY2Y1NmYyNGFmY2FlMTE2M2ExZmNhYjQ0ZDk2Nzc0YTgiCisgIGludGVncml0eSBzaGExLStETDNUTTlXOGtyOHJoRmpvZnlyUk5sbmRLZz0KICAgZGVwZW5kZW5jaWVzOgogICAgIHhtbGRvbSAiPTAuMS4xOSIKICAgICB4cGF0aC5qcyAiPj0wLjAuMyIKIAoteG1sLWVuY3J5cHRpb25AfjAuNy40OgotICB2ZXJzaW9uICIwLjcuNCIKLSAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sLWVuY3J5cHRpb24vLS94bWwtZW5jcnlwdGlvbi0wLjcuNC50Z3ojNDI3OTFlYzY0ZDU1NmQyNDU1ZGNiOWRhMGE1NDEyMzY2NWFjNjVjNyIKK3htbC1lbmNyeXB0aW9uQF4wLjExLjA6CisgIHZlcnNpb24gIjAuMTEuMiIKKyAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sLWVuY3J5cHRpb24vLS94bWwtZW5jcnlwdGlvbi0wLjExLjIudGd6I2MyMTdmNTUwOTU0N2UzNGI1MDBiODI5ZjJjMGJjYTg1Y2NhNzNhMjEiCisgIGludGVncml0eSBzaGE1MTItalZ2RVM3aTVvdmRPN04rTmpnbmNBMzI2eFlLamhxZUFubnZJZ1JuWTdST0xDZkZxRURMd1AwU3hwLzMwU0hHMEFYUVYxMDQ4VDV5aW5PRnl2d0dGemc9PQogICBkZXBlbmRlbmNpZXM6Ci0gICAgYXN5bmMgIn4wLjIuNyIKLSAgICBlanMgIn4wLjguMyIKLSAgICBub2RlLWZvcmdlICIwLjIuMjQiCisgICAgYXN5bmMgIl4yLjEuNSIKKyAgICBlanMgIl4yLjUuNiIKKyAgICBub2RlLWZvcmdlICJeMC43LjAiCiAgICAgeG1sZG9tICJ+MC4xLjE1IgotICAgIHhwYXRoICIwLjAuNSIKKyAgICB4cGF0aCAiMC4wLjI3IgogCi14bWwyanNAfjAuNC4xOgoreG1sMmpzQF4wLjQuMDoKICAgdmVyc2lvbiAiMC40LjE5IgogICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94bWwyanMvLS94bWwyanMtMC40LjE5LnRneiM2ODZjMjBmMjEzMjA5ZTk0YWJmMGQxYmNmMWVmYWEyOTFjNzgyN2E3IgorICBpbnRlZ3JpdHkgc2hhNTEyLWVzWm5KWkpPaUpSOXdXS015dXZTRTF5NkRxNUxDdUphbnFoeHNsSDJieE02ZHVhaE5aK0hNcENMaEJRR1prYlg2eFJmOHgxWTJlSmxndDJxM3FvNDlRPT0KICAgZGVwZW5kZW5jaWVzOgogICAgIHNheCAiPj0wLjYuMCIKICAgICB4bWxidWlsZGVyICJ+OS4wLjEiCiAKLXhtbGJ1aWxkZXJAfjIuMS4wOgotICB2ZXJzaW9uICIyLjEuMCIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcveG1sYnVpbGRlci8tL3htbGJ1aWxkZXItMi4xLjAudGd6IzZkZGFlMzE2ODNiNmRmMTIxMDBiMjlmYzhhMGQ0ZjQ2MzQ5YWJiZWQiCit4bWxidWlsZGVyQH4yLjIuMDoKKyAgdmVyc2lvbiAiMi4yLjEiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGJ1aWxkZXIvLS94bWxidWlsZGVyLTIuMi4xLnRneiM5MzI2NDMwZjEzMGQ4NzQzNWQ0YzQwODY2NDNhYTI5MjZlMTA1YTMyIgorICBpbnRlZ3JpdHkgc2hhMS1reVpERHhNTmgwTmRURUNHWkRxaWttNFFXakk9CiAgIGRlcGVuZGVuY2llczoKLSAgICB1bmRlcnNjb3JlICI+PTEuNS54IgorICAgIGxvZGFzaC1ub2RlICJ+Mi40LjEiCiAKIHhtbGJ1aWxkZXJAfjkuMC4xOgogICB2ZXJzaW9uICI5LjAuNyIKLSAgcmVzb2x2ZWQgImh0dHA6Ly9yZWdpc3RyeS5ucG1qcy5vcmcveG1sYnVpbGRlci8tL3htbGJ1aWxkZXItOS4wLjcudGd6IzEzMmVlNjNkMmVjNTU2NWM1NTdlMjBmNGMyMmRmOWFjYTY4NmIxMGQiCisgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGJ1aWxkZXIvLS94bWxidWlsZGVyLTkuMC43LnRneiMxMzJlZTYzZDJlYzU1NjVjNTU3ZTIwZjRjMjJkZjlhY2E2ODZiMTBkIgorICBpbnRlZ3JpdHkgc2hhMS1FeTdtUFM3RlZseFZmaUQwd2kzNXJLYUdzUTA9CiAKIHhtbGRvbUA9MC4xLjE5OgogICB2ZXJzaW9uICIwLjEuMTkiCiAgIHJlc29sdmVkICJodHRwczovL3JlZ2lzdHJ5Lnlhcm5wa2cuY29tL3htbGRvbS8tL3htbGRvbS0wLjEuMTkudGd6IzYzMWZjMDc3NzZlZmQ4NDExOGJmMjUxNzFiMzdlZDRkMDc1YTBhYmMiCisgIGludGVncml0eSBzaGExLVl4L0FkM2J2MkVFWXZ5VVhHemZ0VFFkYUNydz0KIAoteG1sZG9tQH4wLjEuMTUsIHhtbGRvbUB+MC4xLjE5OgoreG1sZG9tQF4wLjEuMCwgeG1sZG9tQH4wLjEuMTU6CiAgIHZlcnNpb24gIjAuMS4yNyIKICAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veG1sZG9tLy0veG1sZG9tLTAuMS4yNy50Z3ojZDUwMWY5N2IzYmRiNDAzYWY4ZWY5ZWNjMjA1NzMxODdhYWRhYzBlOSIKKyAgaW50ZWdyaXR5IHNoYTEtMVFINWV6dmJRRHI0NzU3TUlGY3hoNnJhd09rPQogCiB4cGF0aC5qc0A+PTAuMC4zOgogICB2ZXJzaW9uICIxLjEuMCIKICAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veHBhdGguanMvLS94cGF0aC5qcy0xLjEuMC50Z3ojMzgxNmE0NGVkNGJiMzUyMDkxMDgzZDAwMmEzODNkZDUxMDRhNWZmMSIKKyAgaW50ZWdyaXR5IHNoYTUxMi1qZytxa2ZTNEs4RTc5NjVzcWFVbDhtUm5nWGlLYjNXWkdmT05nRTE4cHIwM0ZVUWl1U1Y2RytFajR0UzU1QitySVFTRkVJdzNwaGRWQVE0cFBxTldmUT09CiAKLXhwYXRoQDAuMC41OgotICB2ZXJzaW9uICIwLjAuNSIKLSAgcmVzb2x2ZWQgImh0dHBzOi8vcmVnaXN0cnkueWFybnBrZy5jb20veHBhdGgvLS94cGF0aC0wLjAuNS50Z3ojNDU0MDM2ZjZlZjBmM2RmNWFmNWQ0YmE0YTExOWZiNzU2NzRiM2U2YyIKK3hwYXRoQDAuMC4yNzoKKyAgdmVyc2lvbiAiMC4wLjI3IgorICByZXNvbHZlZCAiaHR0cHM6Ly9yZWdpc3RyeS55YXJucGtnLmNvbS94cGF0aC8tL3hwYXRoLTAuMC4yNy50Z3ojZGQzNDIxZmJkY2M1NjQ2YWMzMmM0ODUzMWI0ZDdlOWQwYzJjZmE5MiIKKyAgaW50ZWdyaXR5IHNoYTUxMi1mZzAzV1J4dGtDVjZvaENsZVBOQUVDWXNtcEtLVHY1TDh5L1gzRG4xaFFyZWMzUE94MmpIWi8wUDJxUTZIdnNyVTFCbWVxWGNvZjNOR0d1ZUc2THh3UT09Cg=="
+ }
+ ]
+}
diff --git a/spec/fixtures/security-reports/remediations/remediation.patch b/spec/fixtures/security-reports/remediations/remediation.patch
new file mode 100644
index 00000000000..bbfb6874627
--- /dev/null
+++ b/spec/fixtures/security-reports/remediations/remediation.patch
@@ -0,0 +1,180 @@
+diff --git a/yarn.lock b/yarn.lock
+index 0ecc92f..7fa4554 100644
+--- a/yarn.lock
++++ b/yarn.lock
+@@ -2,103 +2,124 @@
+ # yarn lockfile v1
+
+
+-async@~0.2.7:
+- version "0.2.10"
+- resolved "http://registry.npmjs.org/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
+-
+-async@~1.5.2:
+- version "1.5.2"
+- resolved "http://registry.npmjs.org/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
++async@^2.1.5, async@^2.5.0:
++ version "2.6.1"
++ resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
++ integrity sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==
++ dependencies:
++ lodash "^4.17.10"
+
+-debug@^1.0.4:
+- version "1.0.5"
+- resolved "https://registry.yarnpkg.com/debug/-/debug-1.0.5.tgz#f7241217430f99dec4c2b473eab92228e874c2ac"
++debug@^2.6.0:
++ version "2.6.9"
++ resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
++ integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+ dependencies:
+ ms "2.0.0"
+
+-ejs@~0.8.3:
+- version "0.8.8"
+- resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.8.tgz#ffdc56dcc35d02926dd50ad13439bbc54061d598"
++ejs@^2.5.6:
++ version "2.6.1"
++ resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.6.1.tgz#498ec0d495655abc6f23cd61868d926464071aa0"
++ integrity sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==
++
++lodash-node@~2.4.1:
++ version "2.4.1"
++ resolved "https://registry.yarnpkg.com/lodash-node/-/lodash-node-2.4.1.tgz#ea82f7b100c733d1a42af76801e506105e2a80ec"
++ integrity sha1-6oL3sQDHM9GkKvdoAeUGEF4qgOw=
++
++lodash@^4.17.10:
++ version "4.17.11"
++ resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
++ integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
+
+ ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
++ integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+
+-node-forge@0.2.24:
+- version "0.2.24"
+- resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.2.24.tgz#fa6f846f42fa93f63a0a30c9fbff7b4e130e0858"
++node-forge@^0.7.0:
++ version "0.7.6"
++ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac"
++ integrity sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==
+
+ saml2-js@^1.5.0:
+- version "1.5.0"
+- resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.5.0.tgz#c0d2268a179e7329d29eb25aa82df5503774b0d9"
++ version "1.12.4"
++ resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.12.4.tgz#c288f20bda6d2b91073b16c94ea72f22349ac3b3"
++ integrity sha1-wojyC9ptK5EHOxbJTqcvIjSaw7M=
+ dependencies:
+- async "~1.5.2"
+- debug "^1.0.4"
+- underscore "~1.6.0"
+- xml-crypto "^0.8.1"
+- xml-encryption "~0.7.4"
+- xml2js "~0.4.1"
+- xmlbuilder "~2.1.0"
+- xmldom "~0.1.19"
++ async "^2.5.0"
++ debug "^2.6.0"
++ underscore "^1.8.0"
++ xml-crypto "^0.10.0"
++ xml-encryption "^0.11.0"
++ xml2js "^0.4.0"
++ xmlbuilder "~2.2.0"
++ xmldom "^0.1.0"
+
+ sax@>=0.6.0:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
++ integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
+
+-underscore@>=1.5.x:
++underscore@^1.8.0:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
++ integrity sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==
+
+-underscore@~1.6.0:
+- version "1.6.0"
+- resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
+-
+-xml-crypto@^0.8.1:
+- version "0.8.5"
+- resolved "http://registry.npmjs.org/xml-crypto/-/xml-crypto-0.8.5.tgz#2bbcfb3eb33f3a82a218b822bf672b6b1c20e538"
++xml-crypto@^0.10.0:
++ version "0.10.1"
++ resolved "https://registry.yarnpkg.com/xml-crypto/-/xml-crypto-0.10.1.tgz#f832f74ccf56f24afcae1163a1fcab44d96774a8"
++ integrity sha1-+DL3TM9W8kr8rhFjofyrRNlndKg=
+ dependencies:
+ xmldom "=0.1.19"
+ xpath.js ">=0.0.3"
+
+-xml-encryption@~0.7.4:
+- version "0.7.4"
+- resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.7.4.tgz#42791ec64d556d2455dcb9da0a54123665ac65c7"
++xml-encryption@^0.11.0:
++ version "0.11.2"
++ resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.11.2.tgz#c217f5509547e34b500b829f2c0bca85cca73a21"
++ integrity sha512-jVvES7i5ovdO7N+NjgncA326xYKjhqeAnnvIgRnY7ROLCfFqEDLwP0Sxp/30SHG0AXQV1048T5yinOFyvwGFzg==
+ dependencies:
+- async "~0.2.7"
+- ejs "~0.8.3"
+- node-forge "0.2.24"
++ async "^2.1.5"
++ ejs "^2.5.6"
++ node-forge "^0.7.0"
+ xmldom "~0.1.15"
+- xpath "0.0.5"
++ xpath "0.0.27"
+
+-xml2js@~0.4.1:
++xml2js@^0.4.0:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
++ integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==
+ dependencies:
+ sax ">=0.6.0"
+ xmlbuilder "~9.0.1"
+
+-xmlbuilder@~2.1.0:
+- version "2.1.0"
+- resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.1.0.tgz#6ddae31683b6df12100b29fc8a0d4f46349abbed"
++xmlbuilder@~2.2.0:
++ version "2.2.1"
++ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-2.2.1.tgz#9326430f130d87435d4c4086643aa2926e105a32"
++ integrity sha1-kyZDDxMNh0NdTECGZDqikm4QWjI=
+ dependencies:
+- underscore ">=1.5.x"
++ lodash-node "~2.4.1"
+
+ xmlbuilder@~9.0.1:
+ version "9.0.7"
+- resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
++ resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
++ integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=
+
+ xmldom@=0.1.19:
+ version "0.1.19"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
++ integrity sha1-Yx/Ad3bv2EEYvyUXGzftTQdaCrw=
+
+-xmldom@~0.1.15, xmldom@~0.1.19:
++xmldom@^0.1.0, xmldom@~0.1.15:
+ version "0.1.27"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
++ integrity sha1-1QH5ezvbQDr4757MIFcxh6rawOk=
+
+ xpath.js@>=0.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1"
++ integrity sha512-jg+qkfS4K8E7965sqaUl8mRngXiKb3WZGfONgE18pr03FUQiuSV6G+Ej4tS55B+rIQSFEIw3phdVAQ4pPqNWfQ==
+
+-xpath@0.0.5:
+- version "0.0.5"
+- resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.5.tgz#454036f6ef0f3df5af5d4ba4a119fb75674b3e6c"
++xpath@0.0.27:
++ version "0.0.27"
++ resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.27.tgz#dd3421fbdcc5646ac32c48531b4d7e9d0c2cfa92"
++ integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ==
diff --git a/spec/fixtures/security-reports/remediations/yarn.lock b/spec/fixtures/security-reports/remediations/yarn.lock
new file mode 100644
index 00000000000..0ecc92fb711
--- /dev/null
+++ b/spec/fixtures/security-reports/remediations/yarn.lock
@@ -0,0 +1,104 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+async@~0.2.7:
+ version "0.2.10"
+ resolved "http://registry.npmjs.org/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
+
+async@~1.5.2:
+ version "1.5.2"
+ resolved "http://registry.npmjs.org/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
+
+debug@^1.0.4:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-1.0.5.tgz#f7241217430f99dec4c2b473eab92228e874c2ac"
+ dependencies:
+ ms "2.0.0"
+
+ejs@~0.8.3:
+ version "0.8.8"
+ resolved "https://registry.yarnpkg.com/ejs/-/ejs-0.8.8.tgz#ffdc56dcc35d02926dd50ad13439bbc54061d598"
+
+ms@2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
+
+node-forge@0.2.24:
+ version "0.2.24"
+ resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.2.24.tgz#fa6f846f42fa93f63a0a30c9fbff7b4e130e0858"
+
+saml2-js@^1.5.0:
+ version "1.5.0"
+ resolved "https://registry.yarnpkg.com/saml2-js/-/saml2-js-1.5.0.tgz#c0d2268a179e7329d29eb25aa82df5503774b0d9"
+ dependencies:
+ async "~1.5.2"
+ debug "^1.0.4"
+ underscore "~1.6.0"
+ xml-crypto "^0.8.1"
+ xml-encryption "~0.7.4"
+ xml2js "~0.4.1"
+ xmlbuilder "~2.1.0"
+ xmldom "~0.1.19"
+
+sax@>=0.6.0:
+ version "1.2.4"
+ resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
+
+underscore@>=1.5.x:
+ version "1.9.1"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961"
+
+underscore@~1.6.0:
+ version "1.6.0"
+ resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.6.0.tgz#8b38b10cacdef63337b8b24e4ff86d45aea529a8"
+
+xml-crypto@^0.8.1:
+ version "0.8.5"
+ resolved "http://registry.npmjs.org/xml-crypto/-/xml-crypto-0.8.5.tgz#2bbcfb3eb33f3a82a218b822bf672b6b1c20e538"
+ dependencies:
+ xmldom "=0.1.19"
+ xpath.js ">=0.0.3"
+
+xml-encryption@~0.7.4:
+ version "0.7.4"
+ resolved "https://registry.yarnpkg.com/xml-encryption/-/xml-encryption-0.7.4.tgz#42791ec64d556d2455dcb9da0a54123665ac65c7"
+ dependencies:
+ async "~0.2.7"
+ ejs "~0.8.3"
+ node-forge "0.2.24"
+ xmldom "~0.1.15"
+ xpath "0.0.5"
+
+xml2js@~0.4.1:
+ version "0.4.19"
+ resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7"
+ dependencies:
+ sax ">=0.6.0"
+ xmlbuilder "~9.0.1"
+
+xmlbuilder@~2.1.0:
+ version "2.1.0"
+ resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-2.1.0.tgz#6ddae31683b6df12100b29fc8a0d4f46349abbed"
+ dependencies:
+ underscore ">=1.5.x"
+
+xmlbuilder@~9.0.1:
+ version "9.0.7"
+ resolved "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d"
+
+xmldom@=0.1.19:
+ version "0.1.19"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.19.tgz#631fc07776efd84118bf25171b37ed4d075a0abc"
+
+xmldom@~0.1.15, xmldom@~0.1.19:
+ version "0.1.27"
+ resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9"
+
+xpath.js@>=0.0.3:
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/xpath.js/-/xpath.js-1.1.0.tgz#3816a44ed4bb352091083d002a383dd5104a5ff1"
+
+xpath@0.0.5:
+ version "0.0.5"
+ resolved "https://registry.yarnpkg.com/xpath/-/xpath-0.0.5.tgz#454036f6ef0f3df5af5d4ba4a119fb75674b3e6c"
diff --git a/spec/fixtures/trace/sample_trace b/spec/fixtures/trace/sample_trace
index 3d8beb0dec2..8f9747f8143 100644
--- a/spec/fixtures/trace/sample_trace
+++ b/spec/fixtures/trace/sample_trace
@@ -1795,7 +1795,7 @@ GroupsController
when requesting a redirected path
returns not found
PUT transfer
- when transfering to a subgroup goes right
+ when transferring to a subgroup goes right
should return a notice (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
should redirect to the new path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
when converting to a root group goes right
@@ -2299,7 +2299,7 @@ Groups::TransferService
should update subgroups path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
should update projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
should create redirect for the subgroups and projects (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
- when transfering a group with nested groups and projects
+ when transferring a group with nested groups and projects
should update subgroups path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
should update projects path (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
should create redirect for the subgroups and projects (PENDING: around hook at ./spec/spec_helper.rb:190 did not execute the example)
@@ -2426,9 +2426,9 @@ Groups::MilestonesController
lists legacy group milestones and group milestones
#show
when there is a title parameter
- searchs for a legacy group milestone
+ searches for a legacy group milestone
when there is not a title parameter
- searchs for a group milestone
+ searches for a group milestone
behaves like milestone tabs
#merge_requests
as html
@@ -3109,11 +3109,11 @@ Pending: (Failures listed here are expected and do not affect your suite's statu
# around hook at ./spec/spec_helper.rb:186 did not execute the example
# ./spec/controllers/groups_controller_spec.rb:129
- 15) GroupsController PUT transfer when transfering to a subgroup goes right should return a notice
+ 15) GroupsController PUT transfer when transferring to a subgroup goes right should return a notice
# around hook at ./spec/spec_helper.rb:190 did not execute the example
# ./spec/controllers/groups_controller_spec.rb:516
- 16) GroupsController PUT transfer when transfering to a subgroup goes right should redirect to the new path
+ 16) GroupsController PUT transfer when transferring to a subgroup goes right should redirect to the new path
# around hook at ./spec/spec_helper.rb:190 did not execute the example
# ./spec/controllers/groups_controller_spec.rb:520
@@ -3301,15 +3301,15 @@ Pending: (Failures listed here are expected and do not affect your suite's statu
# around hook at ./spec/spec_helper.rb:190 did not execute the example
# ./spec/services/groups/transfer_service_spec.rb:341
- 63) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should update subgroups path
+ 63) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with nested groups and projects should update subgroups path
# around hook at ./spec/spec_helper.rb:190 did not execute the example
# ./spec/services/groups/transfer_service_spec.rb:363
- 64) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should update projects path
+ 64) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with nested groups and projects should update projects path
# around hook at ./spec/spec_helper.rb:190 did not execute the example
# ./spec/services/groups/transfer_service_spec.rb:375
- 65) Groups::TransferService#execute when transferring a subgroup into another group when transfering a group with nested groups and projects should create redirect for the subgroups and projects
+ 65) Groups::TransferService#execute when transferring a subgroup into another group when transferring a group with nested groups and projects should create redirect for the subgroups and projects
# around hook at ./spec/spec_helper.rb:190 did not execute the example
# ./spec/services/groups/transfer_service_spec.rb:383
diff --git a/spec/fixtures/valid.po b/spec/fixtures/valid.po
index dbe2f952bad..155b6cbb95d 100644
--- a/spec/fixtures/valid.po
+++ b/spec/fixtures/valid.po
@@ -35,9 +35,6 @@ msgid_plural "%d pipelines"
msgstr[0] "1 pipeline"
msgstr[1] "%d pipelines"
-msgid "A collection of graphs regarding Continuous Integration"
-msgstr "Una colección de gráficos sobre Integración Continua"
-
msgid "About auto deploy"
msgstr "Acerca del auto despliegue"
diff --git a/spec/fixtures/x509_certificate.crt b/spec/fixtures/x509_certificate.crt
new file mode 100644
index 00000000000..8a84890b928
--- /dev/null
+++ b/spec/fixtures/x509_certificate.crt
@@ -0,0 +1,27 @@
+-----BEGIN CERTIFICATE-----
+MIIEnDCCAoQCAQEwDQYJKoZIhvcNAQEFBQAwFDESMBAGA1UEAwwJYXV0aG9yaXR5
+MB4XDTE4MDMxOTE1MjYzMloXDTE5MDMxOTE1MjYzMlowFDESMBAGA1UEAwwJbG9j
+YWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+tcM7iphsLlR
+ccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzAn/eVU4jyVWkaBym6MHa8CiDOro9H
+OXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/2FAgFWzrB2HnYSShiN8tBeeDI5cJ
+ii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UTK37k2kbDQZ41rv1ng2w0AUZt0LRA
+NWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ
++1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNkI+cyv0Gle6tk+CkOfE1m0CvNWlNg
+b8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5xMXpdUCsh22CZZHe/4SeFE64amkf
+1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q/nLdY8haMC6KOtpbAWvKX/Jqq0z1
+nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVrVef0pb2mfdtzjzUrYCP0PtnQExPB
+rocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8RSvAoEUs9VbPiUfN7WAyU1K1rTYH
+KV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRlq07Q5LDz33h9KXw1LZT8MWRinVJf
+RePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEAATANBgkqhkiG9w0BAQUFAAOCAgEA
+Skp0tbvVsg3RG2pX0GP25j0ix+f78zG0+BJ6LiKGMoCIBtGKitfUjBg83ru/ILpa
+fpgrQpNQVUnGQ9tmpnqV605ZBBRUC1CRDsvUnyN6p7+yQAq6Fl+2ZKONHpPk+Bl4
+CIewgdkHjTwTpvIM/1DFVCz4R1FxNjY3uqOVcNDczMYEk2Pn2GZNNN35hUHHxWh4
+89ZvI+XKuRFZq3cDPA60PySeJJpCRScWGgnkdEX1gTtWH3WUlq9llxIvRexyNyzZ
+Yqvcfx5UT75/Pp+JPh9lpUCcKLHeUiadjkiLxu3IcrYa4gYx4lA8jgm7adNEahd0
+oMAHoO9DU6XMo7o6tnQH3xQv9RAbQanjuyJR9N7mwmc59bQ6mW+pxCk843GwT73F
+slseJ1nE1fQQQD7mn/KGjmeWtxY2ElUjTay9ff9/AgJeQYRW+oH0cSdo8WCpc2+G
++LZtLWfBgFLHseRlmarSe2pP8KmbaTd3q7Bu0GekVQOxYcNX59Pj4muQZDVLh8aX
+mSQ+Ifts/ljT649MISHn2AZMR4+BUx63tFcatQhbAGGH5LeFdbaGcaVdsUVyZ9a2
+HBmFWNsgEPtcC+WmNzCXbv7jQsLAJXufKG5MnurJgNf/n5uKCmpGsEJDT/KF1k/3
+x9YnqM7zTyV6un+LS3HjEJvwQmqPWe+vFAeXWGCoWxE=
+-----END CERTIFICATE-----
diff --git a/spec/fixtures/x509_certificate_pk.key b/spec/fixtures/x509_certificate_pk.key
new file mode 100644
index 00000000000..c02a3cf6189
--- /dev/null
+++ b/spec/fixtures/x509_certificate_pk.key
@@ -0,0 +1,51 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIJKAIBAAKCAgEA+tcM7iphsLlRccUph2ixabRYnw1HeLCiA4O9a4O31oVUBuzA
+n/eVU4jyVWkaBym6MHa8CiDOro9HOXodITMw+3G1sG/yQZ8Y/5dsOP2hEoSfs63/
+2FAgFWzrB2HnYSShiN8tBeeDI5cJii4JVMfpfi9cvXZUXFR8+P0XR1HDxx6or6UT
+K37k2kbDQZ41rv1ng2w0AUZt0LRANWVE48zvUWIU0y+2JLP1yhrKj85RRjQc5cMK
+88zzWSZBcSjDGGeJ4C8B5Zh2gFlQ+1aJkyyklORR3v/RyYO9prTeXPqQ3x/nNsNk
+I+cyv0Gle6tk+CkOfE1m0CvNWlNgb8LdQ0XZsOYLZvxfpHk3gHA5GrHXvn5StkM5
+xMXpdUCsh22CZZHe/4SeFE64amkf1/LuqY0LYc5UdG2SeJ0SDauPRAIuAr4OV7+Q
+/nLdY8haMC6KOtpbAWvKX/Jqq0z1nUXzQn1JWCNw1QMdq9Uz8wiWOjLTr2D/mIVr
+Vef0pb2mfdtzjzUrYCP0PtnQExPBrocP6BDXN7Ragcdis5/IfLuCOD6pAkmzy6o8
+RSvAoEUs9VbPiUfN7WAyU1K1rTYHKV+zPfWF254nZ2SBeReN9CMKbMJE+TX2chRl
+q07Q5LDz33h9KXw1LZT8MWRinVJfRePsQiyHpRBWRG0AhbD+YpiGKHzsat0CAwEA
+AQKCAgBf1urJ1Meeji/gGETVx9qBWLbDjn9QTayZSyyEd78155tDShIPDLmxQRHW
+MGIReo/5FGSkOgS+DWBZRZ77oGOGrtuMnjkheXhDr8dZvw5b1PBv5ntqWrLnfMYP
+/Ag7xZMyiJLbPqmMX5j1gsFt8zPzUoVMnnl9DYryV0Edrs/utHgfJCM+6yzleUQB
+PkGkqo1yWVVFZ3Nt2nDt9dNsdlC594+dYQ1m2JuArNvYNiw3dpHT98GnhRc1aLh4
+U+q22FiFn3BKGQat43JdlaLa6KO5f8MIQRYWuI8tss2DGPlhRv9AnUcVsLBjAuIH
+bmUVrBosxCYUQ6giatjd2sZPfdC+VIDCbIWRthxkXJ9I/Ap8R98xx/7qIcPFc+XA
+hcK1xOM7zIq2xgAOFeeh8O8Wq9cH8NmUhMCgzIE0WT32Zo0JAW6l0kZc82Y/Yofz
+U+TJKo0NOFZe687HOhanOHbbQSG29XOqxMYTABZ7Ixf+4RZPD5+yQgZWP1BhLluy
+PxZhsLl67xvbfB2i9VVorMN7PbFx5hbni3C7/p63Z0rG5q4/uJBbX3Uuh6KdhIo+
+Zh9UC6u29adIthdxz+ZV5wBccTOgaeHB9wRL9Hbp6ZxyqesQB4RTsFtPNXxZ7K43
+fmJgHZvHhF5gSbeB8JAeBf0cy3pytJM49ZxplifeGVzUJP2gAQKCAQEA/1T9quz5
+sOD03FxV//oRWD1kqfunq3v56sIBG4ZMVZKUqc6wLjTmeklLYKq85AWX8gnCHi0g
+nmG/xDh/rt1/IngMWP98WVuD67hFbrj87g7A7YGIiwZ2gi6hqhqmALN+5JjCSTPp
+XOiPvNnXP0XM4gIHBXV8diHq5rF9NsSh4vx3OExr8KQqVzWoDcnnWNfnDlrFB8cq
+ViII+UqdovXp59hAVOsc+pYAe+8JeQDX17H3U/NMkUw4gU2aWUCvUVjxi9oBG/CW
+ncIdYuW8zne4qXbX7YLC0QUUIDVOWzhLauAUBduTqRTldJo0KAxu887tf+uStXs8
+RACLGIaBQw7BXQKCAQEA+38NFnpflKquU92xRtmqWAVaW7rm865ZO6EIaS4JII/N
+/Ebu1YZrAhT0ruGJQaolYj8w79BEZRF2CYDPZxKFv/ye0O7rWCAGtCdWQ0BXcrIU
+7SdlsdfTNXO1R3WbwCyVxyjg6YF7FjbTaaOAoTiosTjDs2ZOgkbdh/sMeWkSN5HB
+aQz4c8rqq0kkYucLqp4nWYSWSJn88bL8ctwEwW77MheJiSpo1ohNRP3ExHnbCbYw
+RIj7ATSz74ebpd9NMauB5clvMMh4jRG0EQyt7KCoOyfPRFc3fddvTr03LlgFfX/n
+qoxd2nejgAS3NnG1XMxdcUa7cPannt46Sef1uZo3gQKCAQB454zquCYQDKXGBu8u
+NAKsjv2wxBqESENyV4VgvDo/NxawRdAFQUV12GkaEB87ti5aDSbfVS0h8lV1G+/S
+JM5DyybFqcz/Hyebofk20d/q9g+DJ5g5hMjvIhepTc8Xe+d1ZaRyN2Oke/c8TMbx
+DiNTTfR3MEfMRIlPzfHl0jx6GGR3wzBFleb6vsyiIt4qoqmlkXPFGBlDCgDH0v5M
+ITgucacczuw8+HSoOut4Yd7TI1FjbkzubHJBQDb7VnbuBTjzqTpnOYiIkVeK8hBy
+kBxgGodqz0Vi5o2+Jp/A8Co+JHc2wt/r65ovmali4WhUiMLLlQg2aXGDHeK/rUle
+MIl9AoIBAQCPKCYSCnyHypRK5uG3W8VsLzfdCUnXogHnQGXiQTMu1szA8ruWzdnx
+qG4TcgxIVYrMHv5DNAEKquLOzATDPjbmLu1ULvvGAQzv1Yhz5ZchkZ7507g+gIUY
+YxHoaFjNDlP/txQ3tt2SqoizFD/vBap4nsA/SVgdLiuB8PSL07Rr70rx+lEe0H2+
+HHda2Pu6FiZ9/Uvybb0e8+xhkT4fwYW5YM6IRpzAqXuabv1nfZmiMJPPH04JxK88
+BKwjwjVVtbPOUlg5o5ODcXVXUylZjaXVbna8Bw1uU4hngKt9dNtDMeB0I0x1RC7M
+e2Ky2g0LksUJ6uJdjfmiJAt38FLeYJuBAoIBAC2oqaqr86Dug5v8xHpgFoC5u7z7
+BRhaiHpVrUr+wnaNJEXfAEmyKf4xF5xDJqldnYG3c9ETG/7bLcg1dcrMPzXx94Si
+MI3ykwiPeI/sVWYmUlq4U8zCIC7MY6sWzWt3oCBNoCN/EeYx9e7+eLNBB+fADAXq
+v9RMGlUIy7beX0uac8Bs771dsxIb/RrYw58wz+jrwGlzuDmcPWiu+ARu7hnBqCAV
+AITlCV/tsEk7u08oBuv47+rVGCh1Qb19pNswyTtTZARAGErJO0Q+39BNuu0M2TIn
+G3M8eNmGHC+mNsZTVgKRuyk9Ye0s4Bo0KcqSndiPFGHjcrF7/t+RqEOXr/E=
+-----END RSA PRIVATE KEY-----
diff --git a/spec/frontend/.eslintrc.yml b/spec/frontend/.eslintrc.yml
index 046215e4c93..e78a38d31f5 100644
--- a/spec/frontend/.eslintrc.yml
+++ b/spec/frontend/.eslintrc.yml
@@ -2,8 +2,15 @@
env:
jest/globals: true
plugins:
-- jest
+ - jest
+extends:
+ - 'plugin:jest/recommended'
settings:
import/resolver:
jest:
- jestConfigFile: "jest.config.js"
+ jestConfigFile: 'jest.config.js'
+globals:
+ getJSONFixture: false
+ loadFixtures: false
+ preloadFixtures: false
+ setFixtures: false
diff --git a/spec/frontend/__mocks__/file_mock.js b/spec/frontend/__mocks__/file_mock.js
new file mode 100644
index 00000000000..08d725cd4e4
--- /dev/null
+++ b/spec/frontend/__mocks__/file_mock.js
@@ -0,0 +1 @@
+export default '';
diff --git a/spec/javascripts/activities_spec.js b/spec/frontend/activities_spec.js
index 068b8eb65bc..d14be3a1f26 100644
--- a/spec/javascripts/activities_spec.js
+++ b/spec/frontend/activities_spec.js
@@ -1,13 +1,12 @@
/* eslint-disable no-unused-expressions, no-prototype-builtins, no-new, no-shadow */
import $ from 'jquery';
-import 'vendor/jquery.endless-scroll';
import Activities from '~/activities';
import Pager from '~/pager';
describe('Activities', () => {
window.gon || (window.gon = {});
- const fixtureTemplate = 'static/event_filter.html.raw';
+ const fixtureTemplate = 'static/event_filter.html';
const filters = [
{
id: 'all',
@@ -40,7 +39,7 @@ describe('Activities', () => {
beforeEach(() => {
loadFixtures(fixtureTemplate);
- spyOn(Pager, 'init').and.stub();
+ jest.spyOn(Pager, 'init').mockImplementation(() => {});
new Activities();
});
diff --git a/spec/javascripts/api_spec.js b/spec/frontend/api_spec.js
index 1e9470970ff..6010488d9e0 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -4,7 +4,7 @@ import Api from '~/api';
describe('Api', () => {
const dummyApiVersion = 'v3000';
- const dummyUrlRoot = 'http://host.invalid';
+ const dummyUrlRoot = '/gitlab';
const dummyGon = {
api_version: dummyApiVersion,
relative_url_root: dummyUrlRoot,
@@ -32,6 +32,18 @@ describe('Api', () => {
expect(builtUrl).toEqual(expectedOutput);
});
+
+ [null, '', '/'].forEach(root => {
+ it(`works when relative_url_root is ${root}`, () => {
+ window.gon.relative_url_root = root;
+ const input = '/api/:version/foo/bar';
+ const expectedOutput = `/api/${dummyApiVersion}/foo/bar`;
+
+ const builtUrl = Api.buildUrl(input);
+
+ expect(builtUrl).toEqual(expectedOutput);
+ });
+ });
});
describe('group', () => {
@@ -139,6 +151,40 @@ describe('Api', () => {
});
});
+ describe('projectMergeRequests', () => {
+ const projectPath = 'abc';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests`;
+
+ it('fetches all merge requests for a project', done => {
+ const mockData = [{ source_branch: 'foo' }, { source_branch: 'bar' }];
+ mock.onGet(expectedUrl).reply(200, mockData);
+ Api.projectMergeRequests(projectPath)
+ .then(({ data }) => {
+ expect(data.length).toEqual(2);
+ expect(data[0].source_branch).toBe('foo');
+ expect(data[1].source_branch).toBe('bar');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('fetches merge requests filtered with passed params', done => {
+ const params = {
+ source_branch: 'bar',
+ };
+ const mockData = [{ source_branch: 'bar' }];
+ mock.onGet(expectedUrl, { params }).reply(200, mockData);
+
+ Api.projectMergeRequests(projectPath, params)
+ .then(({ data }) => {
+ expect(data.length).toEqual(1);
+ expect(data[0].source_branch).toBe('bar');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('projectMergeRequest', () => {
it('fetches a merge request', done => {
const projectPath = 'abc';
@@ -218,7 +264,7 @@ describe('Api', () => {
const namespace = 'some namespace';
const project = 'some project';
const labelData = { some: 'data' };
- const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/labels`;
+ const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/-/labels`;
const expectedData = {
label: labelData,
};
@@ -242,7 +288,7 @@ describe('Api', () => {
it('creates a group label', done => {
const namespace = 'group/subgroup';
const labelData = { some: 'data' };
- const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`;
+ const expectedUrl = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespace);
const expectedData = {
label: labelData,
};
@@ -413,7 +459,7 @@ describe('Api', () => {
dummyProjectPath,
)}/repository/branches`;
- spyOn(axios, 'post').and.callThrough();
+ jest.spyOn(axios, 'post');
mock.onPost(expectedUrl).replyOnce(200, {
name: branch,
diff --git a/spec/javascripts/autosave_spec.js b/spec/frontend/autosave_spec.js
index dcb1c781591..4d9c8f96d62 100644
--- a/spec/javascripts/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -1,16 +1,19 @@
import $ from 'jquery';
import Autosave from '~/autosave';
import AccessorUtilities from '~/lib/utils/accessor';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
describe('Autosave', () => {
+ useLocalStorageSpy();
+
let autosave;
const field = $('<textarea></textarea>');
const key = 'key';
describe('class constructor', () => {
beforeEach(() => {
- spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
- spyOn(Autosave.prototype, 'restore');
+ jest.spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').mockReturnValue(true);
+ jest.spyOn(Autosave.prototype, 'restore').mockImplementation(() => {});
});
it('should set .isLocalStorageAvailable', () => {
@@ -27,8 +30,6 @@ describe('Autosave', () => {
field,
key,
};
-
- spyOn(window.localStorage, 'getItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
@@ -55,7 +56,7 @@ describe('Autosave', () => {
});
it('triggers jquery event', () => {
- spyOn(autosave.field, 'trigger').and.callThrough();
+ jest.spyOn(autosave.field, 'trigger').mockImplementation(() => {});
Autosave.prototype.restore.call(autosave);
@@ -77,7 +78,7 @@ describe('Autosave', () => {
});
it('does not trigger event', () => {
- spyOn(field, 'trigger').and.callThrough();
+ jest.spyOn(field, 'trigger');
expect(field.trigger).not.toHaveBeenCalled();
});
@@ -86,11 +87,9 @@ describe('Autosave', () => {
describe('save', () => {
beforeEach(() => {
- autosave = jasmine.createSpyObj('autosave', ['reset']);
+ autosave = { reset: jest.fn() };
autosave.field = field;
field.val('value');
-
- spyOn(window.localStorage, 'setItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
@@ -123,8 +122,6 @@ describe('Autosave', () => {
autosave = {
key,
};
-
- spyOn(window.localStorage, 'removeItem');
});
describe('if .isLocalStorageAvailable is `false`', () => {
diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/frontend/behaviors/secret_values_spec.js
index 5aaab093c0c..5aaab093c0c 100644
--- a/spec/javascripts/behaviors/secret_values_spec.js
+++ b/spec/frontend/behaviors/secret_values_spec.js
diff --git a/spec/javascripts/blob/blob_fork_suggestion_spec.js b/spec/frontend/blob/blob_fork_suggestion_spec.js
index 9b81b7e6f92..9b81b7e6f92 100644
--- a/spec/javascripts/blob/blob_fork_suggestion_spec.js
+++ b/spec/frontend/blob/blob_fork_suggestion_spec.js
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/frontend/boards/modal_store_spec.js
index 3257a3fb8a3..4dd27e94d97 100644
--- a/spec/javascripts/boards/modal_store_spec.js
+++ b/spec/frontend/boards/modal_store_spec.js
@@ -1,7 +1,7 @@
/* global ListIssue */
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import Store from '~/boards/stores/modal_store';
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
new file mode 100644
index 00000000000..d23393db60d
--- /dev/null
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -0,0 +1,67 @@
+import actions from '~/boards/stores/actions';
+
+const expectNotImplemented = action => {
+ it('is not implemented', () => {
+ expect(action).toThrow(new Error('Not implemented!'));
+ });
+};
+
+describe('setEndpoints', () => {
+ expectNotImplemented(actions.setEndpoints);
+});
+
+describe('fetchLists', () => {
+ expectNotImplemented(actions.fetchLists);
+});
+
+describe('generateDefaultLists', () => {
+ expectNotImplemented(actions.generateDefaultLists);
+});
+
+describe('createList', () => {
+ expectNotImplemented(actions.createList);
+});
+
+describe('updateList', () => {
+ expectNotImplemented(actions.updateList);
+});
+
+describe('deleteList', () => {
+ expectNotImplemented(actions.deleteList);
+});
+
+describe('fetchIssuesForList', () => {
+ expectNotImplemented(actions.fetchIssuesForList);
+});
+
+describe('moveIssue', () => {
+ expectNotImplemented(actions.moveIssue);
+});
+
+describe('createNewIssue', () => {
+ expectNotImplemented(actions.createNewIssue);
+});
+
+describe('fetchBacklog', () => {
+ expectNotImplemented(actions.fetchBacklog);
+});
+
+describe('bulkUpdateIssues', () => {
+ expectNotImplemented(actions.bulkUpdateIssues);
+});
+
+describe('fetchIssue', () => {
+ expectNotImplemented(actions.fetchIssue);
+});
+
+describe('toggleIssueSubscription', () => {
+ expectNotImplemented(actions.toggleIssueSubscription);
+});
+
+describe('showPage', () => {
+ expectNotImplemented(actions.showPage);
+});
+
+describe('toggleEmptyState', () => {
+ expectNotImplemented(actions.toggleEmptyState);
+});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
new file mode 100644
index 00000000000..aa477766978
--- /dev/null
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -0,0 +1,91 @@
+import mutations from '~/boards/stores/mutations';
+
+const expectNotImplemented = action => {
+ it('is not implemented', () => {
+ expect(action).toThrow(new Error('Not implemented!'));
+ });
+};
+
+describe('SET_ENDPOINTS', () => {
+ expectNotImplemented(mutations.SET_ENDPOINTS);
+});
+
+describe('REQUEST_ADD_LIST', () => {
+ expectNotImplemented(mutations.REQUEST_ADD_LIST);
+});
+
+describe('RECEIVE_ADD_LIST_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_ADD_LIST_SUCCESS);
+});
+
+describe('RECEIVE_ADD_LIST_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_ADD_LIST_ERROR);
+});
+
+describe('REQUEST_UPDATE_LIST', () => {
+ expectNotImplemented(mutations.REQUEST_UPDATE_LIST);
+});
+
+describe('RECEIVE_UPDATE_LIST_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_SUCCESS);
+});
+
+describe('RECEIVE_UPDATE_LIST_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_UPDATE_LIST_ERROR);
+});
+
+describe('REQUEST_REMOVE_LIST', () => {
+ expectNotImplemented(mutations.REQUEST_REMOVE_LIST);
+});
+
+describe('RECEIVE_REMOVE_LIST_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_SUCCESS);
+});
+
+describe('RECEIVE_REMOVE_LIST_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR);
+});
+
+describe('REQUEST_ADD_ISSUE', () => {
+ expectNotImplemented(mutations.REQUEST_ADD_ISSUE);
+});
+
+describe('RECEIVE_ADD_ISSUE_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_SUCCESS);
+});
+
+describe('RECEIVE_ADD_ISSUE_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_ADD_ISSUE_ERROR);
+});
+
+describe('REQUEST_MOVE_ISSUE', () => {
+ expectNotImplemented(mutations.REQUEST_MOVE_ISSUE);
+});
+
+describe('RECEIVE_MOVE_ISSUE_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_SUCCESS);
+});
+
+describe('RECEIVE_MOVE_ISSUE_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_MOVE_ISSUE_ERROR);
+});
+
+describe('REQUEST_UPDATE_ISSUE', () => {
+ expectNotImplemented(mutations.REQUEST_UPDATE_ISSUE);
+});
+
+describe('RECEIVE_UPDATE_ISSUE_SUCCESS', () => {
+ expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_SUCCESS);
+});
+
+describe('RECEIVE_UPDATE_ISSUE_ERROR', () => {
+ expectNotImplemented(mutations.RECEIVE_UPDATE_ISSUE_ERROR);
+});
+
+describe('SET_CURRENT_PAGE', () => {
+ expectNotImplemented(mutations.SET_CURRENT_PAGE);
+});
+
+describe('TOGGLE_EMPTY_STATE', () => {
+ expectNotImplemented(mutations.TOGGLE_EMPTY_STATE);
+});
diff --git a/spec/frontend/boards/stores/state_spec.js b/spec/frontend/boards/stores/state_spec.js
new file mode 100644
index 00000000000..35490a63567
--- /dev/null
+++ b/spec/frontend/boards/stores/state_spec.js
@@ -0,0 +1,11 @@
+import createState from '~/boards/stores/state';
+
+describe('createState', () => {
+ it('is a function', () => {
+ expect(createState).toEqual(expect.any(Function));
+ });
+
+ it('returns an object', () => {
+ expect(createState()).toEqual(expect.any(Object));
+ });
+});
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
new file mode 100644
index 00000000000..6de06a9e2d5
--- /dev/null
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -0,0 +1,387 @@
+import Clusters from '~/clusters/clusters_bundle';
+import {
+ APPLICATION_STATUS,
+ INGRESS_DOMAIN_SUFFIX,
+ APPLICATIONS,
+ RUNNER,
+} from '~/clusters/constants';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import { loadHTMLFixture } from 'helpers/fixtures';
+import { setTestTimeout } from 'helpers/timeout';
+import $ from 'jquery';
+
+const { INSTALLING, INSTALLABLE, INSTALLED, UNINSTALLING } = APPLICATION_STATUS;
+
+describe('Clusters', () => {
+ setTestTimeout(1000);
+
+ let cluster;
+ let mock;
+
+ const mockGetClusterStatusRequest = () => {
+ const { statusPath } = document.querySelector('.js-edit-cluster-form').dataset;
+
+ mock = new MockAdapter(axios);
+
+ mock.onGet(statusPath).reply(200);
+ };
+
+ beforeEach(() => {
+ loadHTMLFixture('clusters/show_cluster.html');
+ });
+
+ beforeEach(() => {
+ mockGetClusterStatusRequest();
+ });
+
+ beforeEach(() => {
+ cluster = new Clusters();
+ });
+
+ afterEach(() => {
+ cluster.destroy();
+ mock.restore();
+ });
+
+ describe('toggle', () => {
+ it('should update the button and the input field on click', done => {
+ const toggleButton = document.querySelector(
+ '.js-cluster-enable-toggle-area .js-project-feature-toggle',
+ );
+ const toggleInput = document.querySelector(
+ '.js-cluster-enable-toggle-area .js-project-feature-toggle-input',
+ );
+
+ $(toggleInput).one('trigger-change', () => {
+ expect(toggleButton.classList).not.toContain('is-checked');
+ expect(toggleInput.getAttribute('value')).toEqual('false');
+ done();
+ });
+
+ toggleButton.click();
+ });
+ });
+
+ describe('showToken', () => {
+ it('should update token field type', () => {
+ cluster.showTokenButton.click();
+
+ expect(cluster.tokenField.getAttribute('type')).toEqual('text');
+
+ cluster.showTokenButton.click();
+
+ expect(cluster.tokenField.getAttribute('type')).toEqual('password');
+ });
+
+ it('should update show token button text', () => {
+ cluster.showTokenButton.click();
+
+ expect(cluster.showTokenButton.textContent).toEqual('Hide');
+
+ cluster.showTokenButton.click();
+
+ expect(cluster.showTokenButton.textContent).toEqual('Show');
+ });
+ });
+
+ describe('checkForNewInstalls', () => {
+ const INITIAL_APP_MAP = {
+ helm: { status: null, title: 'Helm Tiller' },
+ ingress: { status: null, title: 'Ingress' },
+ runner: { status: null, title: 'GitLab Runner' },
+ };
+
+ it('does not show alert when things transition from initial null state to something', () => {
+ cluster.checkForNewInstalls(INITIAL_APP_MAP, {
+ ...INITIAL_APP_MAP,
+ helm: { status: INSTALLABLE, title: 'Helm Tiller' },
+ });
+
+ const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
+
+ expect(flashMessage).toBeNull();
+ });
+
+ it('shows an alert when something gets newly installed', () => {
+ cluster.checkForNewInstalls(
+ {
+ ...INITIAL_APP_MAP,
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
+ },
+ {
+ ...INITIAL_APP_MAP,
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
+ },
+ );
+
+ const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
+
+ expect(flashMessage).not.toBeNull();
+ expect(flashMessage.textContent.trim()).toEqual(
+ 'Helm Tiller was successfully installed on your Kubernetes cluster',
+ );
+ });
+
+ it('shows an alert when multiple things gets newly installed', () => {
+ cluster.checkForNewInstalls(
+ {
+ ...INITIAL_APP_MAP,
+ helm: { status: INSTALLING, title: 'Helm Tiller' },
+ ingress: { status: INSTALLABLE, title: 'Ingress' },
+ },
+ {
+ ...INITIAL_APP_MAP,
+ helm: { status: INSTALLED, title: 'Helm Tiller' },
+ ingress: { status: INSTALLED, title: 'Ingress' },
+ },
+ );
+
+ const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
+
+ expect(flashMessage).not.toBeNull();
+ expect(flashMessage.textContent.trim()).toEqual(
+ 'Helm Tiller, Ingress was successfully installed on your Kubernetes cluster',
+ );
+ });
+ });
+
+ describe('updateContainer', () => {
+ describe('when creating cluster', () => {
+ it('should show the creating container', () => {
+ cluster.updateContainer(null, 'creating');
+
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy();
+
+ expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ });
+
+ it('should continue to show `creating` banner with subsequent updates of the same status', () => {
+ cluster.updateContainer('creating', 'creating');
+
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy();
+
+ expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ });
+ });
+
+ describe('when cluster is created', () => {
+ it('should show the success container and fresh the page', () => {
+ cluster.updateContainer(null, 'created');
+
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.successContainer.classList.contains('hidden')).toBeFalsy();
+
+ expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ });
+
+ it('should not show a banner when status is already `created`', () => {
+ cluster.updateContainer('created', 'created');
+
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
+ });
+ });
+
+ describe('when cluster has error', () => {
+ it('should show the error container', () => {
+ cluster.updateContainer(null, 'errored', 'this is an error');
+
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy();
+
+ expect(cluster.errorReasonContainer.textContent).toContain('this is an error');
+ });
+
+ it('should show `error` banner when previously `creating`', () => {
+ cluster.updateContainer('creating', 'errored');
+
+ expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
+
+ expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy();
+ });
+ });
+
+ describe('when cluster is unreachable', () => {
+ it('should show the unreachable warning container', () => {
+ cluster.updateContainer(null, 'unreachable');
+
+ expect(cluster.unreachableContainer.classList.contains('hidden')).toBe(false);
+ });
+ });
+
+ describe('when cluster has an authentication failure', () => {
+ it('should show the authentication failure warning container', () => {
+ cluster.updateContainer(null, 'authentication_failure');
+
+ expect(cluster.authenticationFailureContainer.classList.contains('hidden')).toBe(false);
+ });
+ });
+ });
+
+ describe('installApplication', () => {
+ it.each(APPLICATIONS)('tries to install %s', applicationId => {
+ jest.spyOn(cluster.service, 'installApplication').mockResolvedValueOnce();
+
+ cluster.store.state.applications[applicationId].status = INSTALLABLE;
+
+ cluster.installApplication({ id: applicationId });
+
+ expect(cluster.store.state.applications[applicationId].status).toEqual(INSTALLING);
+ expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
+ expect(cluster.service.installApplication).toHaveBeenCalledWith(applicationId, undefined);
+ });
+
+ it('sets error request status when the request fails', () => {
+ jest
+ .spyOn(cluster.service, 'installApplication')
+ .mockRejectedValueOnce(new Error('STUBBED ERROR'));
+
+ cluster.store.state.applications.helm.status = INSTALLABLE;
+
+ const promise = cluster.installApplication({ id: 'helm' });
+
+ return promise.then(() => {
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLABLE);
+ expect(cluster.store.state.applications.helm.installFailed).toBe(true);
+
+ expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
+ });
+ });
+ });
+
+ describe('uninstallApplication', () => {
+ it.each(APPLICATIONS)('tries to uninstall %s', applicationId => {
+ jest.spyOn(cluster.service, 'uninstallApplication').mockResolvedValueOnce();
+
+ cluster.store.state.applications[applicationId].status = INSTALLED;
+
+ cluster.uninstallApplication({ id: applicationId });
+
+ expect(cluster.store.state.applications[applicationId].status).toEqual(UNINSTALLING);
+ expect(cluster.store.state.applications[applicationId].requestReason).toEqual(null);
+ expect(cluster.service.uninstallApplication).toHaveBeenCalledWith(applicationId);
+ });
+
+ it('sets error request status when the uninstall request fails', () => {
+ jest
+ .spyOn(cluster.service, 'uninstallApplication')
+ .mockRejectedValueOnce(new Error('STUBBED ERROR'));
+
+ cluster.store.state.applications.helm.status = INSTALLED;
+
+ const promise = cluster.uninstallApplication({ id: 'helm' });
+
+ return promise.then(() => {
+ expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED);
+ expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true);
+
+ expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
+ });
+ });
+ });
+
+ describe('handleSuccess', () => {
+ beforeEach(() => {
+ jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis();
+ jest.spyOn(cluster, 'toggleIngressDomainHelpText').mockReturnThis();
+ jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis();
+ jest.spyOn(cluster, 'updateContainer').mockReturnThis();
+
+ cluster.handleSuccess({ data: {} });
+ });
+
+ it('updates clusters store', () => {
+ expect(cluster.store.updateStateFromServer).toHaveBeenCalled();
+ });
+
+ it('checks for new installable apps', () => {
+ expect(cluster.checkForNewInstalls).toHaveBeenCalled();
+ });
+
+ it('toggles ingress domain help text', () => {
+ expect(cluster.toggleIngressDomainHelpText).toHaveBeenCalled();
+ });
+
+ it('updates message containers', () => {
+ expect(cluster.updateContainer).toHaveBeenCalled();
+ });
+ });
+
+ describe('toggleIngressDomainHelpText', () => {
+ let ingressPreviousState;
+ let ingressNewState;
+
+ beforeEach(() => {
+ ingressPreviousState = { externalIp: null };
+ ingressNewState = { externalIp: '127.0.0.1' };
+ });
+
+ describe(`when ingress have an external ip assigned`, () => {
+ beforeEach(() => {
+ cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
+ });
+
+ it('displays custom domain help text', () => {
+ expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(false);
+ });
+
+ it('updates ingress external ip address', () => {
+ expect(cluster.ingressDomainSnippet.textContent).toEqual(
+ `${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`,
+ );
+ });
+ });
+
+ describe(`when ingress does not have an external ip assigned`, () => {
+ it('hides custom domain help text', () => {
+ ingressPreviousState.externalIp = '127.0.0.1';
+ ingressNewState.externalIp = null;
+ cluster.ingressDomainHelpText.classList.remove('hide');
+
+ cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
+
+ expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true);
+ });
+ });
+ });
+
+ describe('updateApplication', () => {
+ const params = { version: '1.0.0' };
+ let storeUpdateApplication;
+ let installApplication;
+
+ beforeEach(() => {
+ storeUpdateApplication = jest.spyOn(cluster.store, 'updateApplication');
+ installApplication = jest.spyOn(cluster.service, 'installApplication');
+
+ cluster.updateApplication({ id: RUNNER, params });
+ });
+
+ afterEach(() => {
+ storeUpdateApplication.mockRestore();
+ installApplication.mockRestore();
+ });
+
+ it('calls store updateApplication method', () => {
+ expect(storeUpdateApplication).toHaveBeenCalledWith(RUNNER);
+ });
+
+ it('sends installApplication request', () => {
+ expect(installApplication).toHaveBeenCalledWith(RUNNER, params);
+ });
+ });
+});
diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index 8cb9713964e..9f127ccb690 100644
--- a/spec/javascripts/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
import eventHub from '~/clusters/event_hub';
-import { APPLICATION_STATUS, REQUEST_SUBMITTED, REQUEST_FAILURE } from '~/clusters/constants';
+import { APPLICATION_STATUS } from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
+
+import mountComponent from 'helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
describe('Application Row', () => {
@@ -80,17 +83,6 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(false);
});
- it('has loading "Installing" when APPLICATION_STATUS.SCHEDULED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.SCHEDULED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
it('has loading "Installing" when APPLICATION_STATUS.INSTALLING', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
@@ -102,22 +94,12 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true);
});
- it('has loading "Installing" when REQUEST_SUBMITTED', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_SUBMITTED,
- });
-
- expect(vm.installButtonLabel).toEqual('Installing');
- expect(vm.installButtonLoading).toEqual(true);
- expect(vm.installButtonDisabled).toEqual(true);
- });
-
- it('has disabled "Installed" when APPLICATION_STATUS.INSTALLED', () => {
+ it('has disabled "Installed" when application is installed and not uninstallable', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLED,
+ installed: true,
+ uninstallable: false,
});
expect(vm.installButtonLabel).toEqual('Installed');
@@ -125,21 +107,23 @@ describe('Application Row', () => {
expect(vm.installButtonDisabled).toEqual(true);
});
- it('has disabled "Installed" when APPLICATION_STATUS.UPDATING', () => {
+ it('hides when application is installed and uninstallable', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATING,
+ status: APPLICATION_STATUS.INSTALLED,
+ installed: true,
+ uninstallable: true,
});
+ const installBtn = vm.$el.querySelector('.js-cluster-application-install-button');
- expect(vm.installButtonLabel).toEqual('Installed');
- expect(vm.installButtonLoading).toEqual(false);
- expect(vm.installButtonDisabled).toEqual(true);
+ expect(installBtn).toBe(null);
});
- it('has enabled "Install" when APPLICATION_STATUS.ERROR', () => {
+ it('has enabled "Install" when install fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.ERROR,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -151,7 +135,6 @@ describe('Application Row', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
});
expect(vm.installButtonLabel).toEqual('Install');
@@ -160,7 +143,7 @@ describe('Application Row', () => {
});
it('clicking install button emits event', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
@@ -176,7 +159,7 @@ describe('Application Row', () => {
});
it('clicking install button when installApplicationRequestParams are provided emits event', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLABLE,
@@ -193,7 +176,7 @@ describe('Application Row', () => {
});
it('clicking disabled install button emits nothing', () => {
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.INSTALLING,
@@ -208,199 +191,299 @@ describe('Application Row', () => {
});
});
- describe('Upgrade button', () => {
+ describe('Uninstall button', () => {
+ it('displays button when app is installed and uninstallable', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ installed: true,
+ uninstallable: true,
+ status: APPLICATION_STATUS.NOT_INSTALLABLE,
+ });
+ const uninstallButton = vm.$el.querySelector('.js-cluster-application-uninstall-button');
+
+ expect(uninstallButton).toBeTruthy();
+ });
+
+ it('displays a success toast message if application uninstall was successful', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ title: 'GitLab Runner',
+ uninstallSuccessful: false,
+ });
+
+ vm.$toast = { show: jest.fn() };
+ vm.uninstallSuccessful = true;
+
+ return vm.$nextTick(() => {
+ expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner uninstalled successfully.');
+ });
+ });
+ });
+
+ describe('when confirmation modal triggers confirm event', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(ApplicationRow, {
+ propsData: {
+ ...DEFAULT_APPLICATION_STATE,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('triggers uninstallApplication event', () => {
+ jest.spyOn(eventHub, '$emit');
+ wrapper.find(UninstallApplicationConfirmationModal).vm.$emit('confirm');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('uninstallApplication', {
+ id: DEFAULT_APPLICATION_STATE.id,
+ });
+ });
+ });
+
+ describe('Update button', () => {
it('has indeterminate state on page load', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: null,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- expect(upgradeBtn).toBe(null);
+ expect(updateBtn).toBe(null);
});
- it('has enabled "Upgrade" when "upgradeAvailable" is true', () => {
+ it('has enabled "Update" when "updateAvailable" is true', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- upgradeAvailable: true,
+ updateAvailable: true,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- expect(upgradeBtn).not.toBe(null);
- expect(upgradeBtn.innerHTML).toContain('Upgrade');
+ expect(updateBtn).not.toBe(null);
+ expect(updateBtn.innerHTML).toContain('Update');
});
- it('has enabled "Retry upgrade" when APPLICATION_STATUS.UPDATE_ERRORED', () => {
+ it('has enabled "Retry update" when update process fails', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- expect(upgradeBtn).not.toBe(null);
- expect(vm.upgradeFailed).toBe(true);
- expect(upgradeBtn.innerHTML).toContain('Retry upgrade');
+ expect(updateBtn).not.toBe(null);
+ expect(updateBtn.innerHTML).toContain('Retry update');
});
- it('has disabled "Retry upgrade" when APPLICATION_STATUS.UPDATING', () => {
+ it('has disabled "Updating" when APPLICATION_STATUS.UPDATING', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- expect(upgradeBtn).not.toBe(null);
- expect(vm.isUpgrading).toBe(true);
- expect(upgradeBtn.innerHTML).toContain('Upgrading');
+ expect(updateBtn).not.toBe(null);
+ expect(vm.isUpdating).toBe(true);
+ expect(updateBtn.innerHTML).toContain('Updating');
});
- it('clicking upgrade button emits event', () => {
- spyOn(eventHub, '$emit');
+ it('clicking update button emits event', () => {
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateAvailable: true,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- upgradeBtn.click();
+ updateBtn.click();
- expect(eventHub.$emit).toHaveBeenCalledWith('upgradeApplication', {
+ expect(eventHub.$emit).toHaveBeenCalledWith('updateApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: {},
});
});
- it('clicking disabled upgrade button emits nothing', () => {
- spyOn(eventHub, '$emit');
+ it('clicking disabled update button emits nothing', () => {
+ jest.spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_STATUS.UPDATING,
});
- const upgradeBtn = vm.$el.querySelector('.js-cluster-application-upgrade-button');
+ const updateBtn = vm.$el.querySelector('.js-cluster-application-update-button');
- upgradeBtn.click();
+ updateBtn.click();
expect(eventHub.$emit).not.toHaveBeenCalled();
});
- it('displays an error message if application upgrade failed', () => {
+ it('displays an error message if application update failed', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
title: 'GitLab Runner',
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
});
- const failureMessage = vm.$el.querySelector(
- '.js-cluster-application-upgrade-failure-message',
- );
+ const failureMessage = vm.$el.querySelector('.js-cluster-application-update-details');
expect(failureMessage).not.toBe(null);
expect(failureMessage.innerHTML).toContain(
- 'Something went wrong when upgrading GitLab Runner. Please check the logs and try again.',
+ 'Update failed. Please check the logs and try again.',
);
});
+
+ it('displays a success toast message if application update was successful', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ title: 'GitLab Runner',
+ updateSuccessful: false,
+ });
+
+ vm.$toast = { show: jest.fn() };
+ vm.updateSuccessful = true;
+
+ return vm.$nextTick(() => {
+ expect(vm.$toast.show).toHaveBeenCalledWith('GitLab Runner updated successfully.');
+ });
+ });
});
describe('Version', () => {
- it('displays a version number if application has been upgraded', () => {
+ it('displays a version number if application has been updated', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
version,
});
- const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
- const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
+ const updateDetails = vm.$el.querySelector('.js-cluster-application-update-details');
+ const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
- expect(upgradeDetails.innerHTML).toContain('Upgraded');
+ expect(updateDetails.innerHTML).toContain('Updated');
expect(versionEl).not.toBe(null);
expect(versionEl.innerHTML).toContain(version);
});
- it('contains a link to the chart repo if application has been upgraded', () => {
+ it('contains a link to the chart repo if application has been updated', () => {
const version = '0.1.45';
const chartRepo = 'https://gitlab.com/charts/gitlab-runner';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateSuccessful: true,
chartRepo,
version,
});
- const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
+ const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
expect(versionEl.href).toEqual(chartRepo);
expect(versionEl.target).toEqual('_blank');
});
- it('does not display a version number if application upgrade failed', () => {
+ it('does not display a version number if application update failed', () => {
const version = '0.1.45';
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.UPDATE_ERRORED,
+ status: APPLICATION_STATUS.INSTALLED,
+ updateFailed: true,
version,
});
- const upgradeDetails = vm.$el.querySelector('.js-cluster-application-upgrade-details');
- const versionEl = vm.$el.querySelector('.js-cluster-application-upgrade-version');
+ const updateDetails = vm.$el.querySelector('.js-cluster-application-update-details');
+ const versionEl = vm.$el.querySelector('.js-cluster-application-update-version');
- expect(upgradeDetails.innerHTML).toContain('failed');
+ expect(updateDetails.innerHTML).toContain('failed');
expect(versionEl).toBe(null);
});
});
describe('Error block', () => {
- it('does not show error block when there is no error', () => {
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: null,
- requestStatus: null,
+ describe('when nothing fails', () => {
+ it('does not show error block', () => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ });
+ const generalErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-general-error-message',
+ );
+
+ expect(generalErrorMessage).toBeNull();
});
- const generalErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-general-error-message',
- );
-
- expect(generalErrorMessage).toBeNull();
});
- it('shows status reason when APPLICATION_STATUS.ERROR', () => {
+ describe('when install or uninstall fails', () => {
const statusReason = 'We broke it 0.0';
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.ERROR,
- statusReason,
+ const requestReason = 'We broke the request 0.0';
+
+ beforeEach(() => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_STATUS.ERROR,
+ statusReason,
+ requestReason,
+ installFailed: true,
+ });
});
- const generalErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-general-error-message',
- );
- const statusErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-status-error-message',
- );
- expect(generalErrorMessage.textContent.trim()).toEqual(
- `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
- );
+ it('shows status reason if it is available', () => {
+ const statusErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-status-error-message',
+ );
+
+ expect(statusErrorMessage.textContent.trim()).toEqual(statusReason);
+ });
- expect(statusErrorMessage.textContent.trim()).toEqual(statusReason);
+ it('shows request reason if it is available', () => {
+ const requestErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-request-error-message',
+ );
+
+ expect(requestErrorMessage.textContent.trim()).toEqual(requestReason);
+ });
});
- it('shows request reason when REQUEST_FAILURE', () => {
- const requestReason = 'We broke thre request 0.0';
- vm = mountComponent(ApplicationRow, {
- ...DEFAULT_APPLICATION_STATE,
- status: APPLICATION_STATUS.INSTALLABLE,
- requestStatus: REQUEST_FAILURE,
- requestReason,
+ describe('when install fails', () => {
+ beforeEach(() => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_STATUS.ERROR,
+ installFailed: true,
+ });
});
- const generalErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-general-error-message',
- );
- const requestErrorMessage = vm.$el.querySelector(
- '.js-cluster-application-request-error-message',
- );
- expect(generalErrorMessage.textContent.trim()).toEqual(
- `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
- );
+ it('shows a general message indicating the installation failed', () => {
+ const generalErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-general-error-message',
+ );
+
+ expect(generalErrorMessage.textContent.trim()).toEqual(
+ `Something went wrong while installing ${DEFAULT_APPLICATION_STATE.title}`,
+ );
+ });
+ });
+
+ describe('when uninstall fails', () => {
+ beforeEach(() => {
+ vm = mountComponent(ApplicationRow, {
+ ...DEFAULT_APPLICATION_STATE,
+ status: APPLICATION_STATUS.ERROR,
+ uninstallFailed: true,
+ });
+ });
+
+ it('shows a general message indicating the uninstalling failed', () => {
+ const generalErrorMessage = vm.$el.querySelector(
+ '.js-cluster-application-general-error-message',
+ );
- expect(requestErrorMessage.textContent.trim()).toEqual(requestReason);
+ expect(generalErrorMessage.textContent.trim()).toEqual(
+ `Something went wrong while uninstalling ${DEFAULT_APPLICATION_STATE.title}`,
+ );
+ });
});
});
});
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 14ef1193984..221ebb143be 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -1,7 +1,11 @@
import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE } from '~/clusters/constants';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
+import eventHub from '~/clusters/event_hub';
+import { shallowMount } from '@vue/test-utils';
+import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
describe('Applications', () => {
let vm;
@@ -18,16 +22,8 @@ describe('Applications', () => {
describe('Project cluster applications', () => {
beforeEach(() => {
vm = mountComponent(Applications, {
+ applications: APPLICATIONS_MOCK_STATE,
type: CLUSTER_TYPE.PROJECT,
- applications: {
- helm: { title: 'Helm Tiller' },
- ingress: { title: 'Ingress' },
- cert_manager: { title: 'Cert-Manager' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub' },
- knative: { title: 'Knative' },
- },
});
});
@@ -64,15 +60,7 @@ describe('Applications', () => {
beforeEach(() => {
vm = mountComponent(Applications, {
type: CLUSTER_TYPE.GROUP,
- applications: {
- helm: { title: 'Helm Tiller' },
- ingress: { title: 'Ingress' },
- cert_manager: { title: 'Cert-Manager' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub' },
- knative: { title: 'Knative' },
- },
+ applications: APPLICATIONS_MOCK_STATE,
});
});
@@ -89,11 +77,11 @@ describe('Applications', () => {
});
it('renders a row for Prometheus', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeNull();
+ expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).not.toBeNull();
});
it('renders a row for GitLab Runner', () => {
- expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeNull();
+ expect(vm.$el.querySelector('.js-cluster-application-row-runner')).not.toBeNull();
});
it('renders a row for Jupyter', () => {
@@ -111,21 +99,16 @@ describe('Applications', () => {
it('renders ip address with a clipboard button', () => {
vm = mountComponent(Applications, {
applications: {
+ ...APPLICATIONS_MOCK_STATE,
ingress: {
title: 'Ingress',
status: 'installed',
externalIp: '0.0.0.0',
},
- helm: { title: 'Helm Tiller' },
- cert_manager: { title: 'Cert-Manager' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', hostname: '' },
- knative: { title: 'Knative', hostname: '' },
},
});
- expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0');
+ expect(vm.$el.querySelector('.js-endpoint').value).toEqual('0.0.0.0');
expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
@@ -133,13 +116,14 @@ describe('Applications', () => {
});
});
- describe('without ip address', () => {
- it('renders an input text with a question mark and an alert text', () => {
+ describe('with hostname', () => {
+ it('renders hostname with a clipboard button', () => {
vm = mountComponent(Applications, {
applications: {
ingress: {
title: 'Ingress',
status: 'installed',
+ externalHostname: 'localhost.localdomain',
},
helm: { title: 'Helm Tiller' },
cert_manager: { title: 'Cert-Manager' },
@@ -150,9 +134,28 @@ describe('Applications', () => {
},
});
- expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?');
+ expect(vm.$el.querySelector('.js-endpoint').value).toEqual('localhost.localdomain');
- expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
+ expect(
+ vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
+ ).toEqual('localhost.localdomain');
+ });
+ });
+
+ describe('without ip address', () => {
+ it('renders an input text with a loading icon and an alert text', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ ingress: {
+ title: 'Ingress',
+ status: 'installed',
+ },
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-ingress-ip-loading-icon')).not.toBe(null);
+ expect(vm.$el.querySelector('.js-no-endpoint-message')).not.toBe(null);
});
});
});
@@ -160,19 +163,11 @@ describe('Applications', () => {
describe('before installing', () => {
it('does not render the IP address', () => {
vm = mountComponent(Applications, {
- applications: {
- helm: { title: 'Helm Tiller' },
- ingress: { title: 'Ingress' },
- cert_manager: { title: 'Cert-Manager' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', hostname: '' },
- knative: { title: 'Knative', hostname: '' },
- },
+ applications: APPLICATIONS_MOCK_STATE,
});
expect(vm.$el.textContent).not.toContain('Ingress IP Address');
- expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
+ expect(vm.$el.querySelector('.js-endpoint')).toBe(null);
});
});
@@ -181,17 +176,12 @@ describe('Applications', () => {
it('renders email & allows editing', () => {
vm = mountComponent(Applications, {
applications: {
- helm: { title: 'Helm Tiller', status: 'installed' },
- ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ ...APPLICATIONS_MOCK_STATE,
cert_manager: {
title: 'Cert-Manager',
email: 'before@example.com',
status: 'installable',
},
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
- knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -204,17 +194,12 @@ describe('Applications', () => {
it('renders email in readonly', () => {
vm = mountComponent(Applications, {
applications: {
- helm: { title: 'Helm Tiller', status: 'installed' },
- ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ ...APPLICATIONS_MOCK_STATE,
cert_manager: {
title: 'Cert-Manager',
email: 'after@example.com',
status: 'installed',
},
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
- knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -229,13 +214,12 @@ describe('Applications', () => {
it('renders hostname active input', () => {
vm = mountComponent(Applications, {
applications: {
- helm: { title: 'Helm Tiller', status: 'installed' },
- ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
- cert_manager: { title: 'Cert-Manager' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
- knative: { title: 'Knative', hostname: '', status: 'installable' },
+ ...APPLICATIONS_MOCK_STATE,
+ ingress: {
+ title: 'Ingress',
+ status: 'installed',
+ externalIp: '1.1.1.1',
+ },
},
});
@@ -247,13 +231,8 @@ describe('Applications', () => {
it('does not render hostname input', () => {
vm = mountComponent(Applications, {
applications: {
- helm: { title: 'Helm Tiller', status: 'installed' },
+ ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed' },
- cert_manager: { title: 'Cert-Manager' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', hostname: '', status: 'installable' },
- knative: { title: 'Knative', hostname: '', status: 'installable' },
},
});
@@ -265,13 +244,9 @@ describe('Applications', () => {
it('renders readonly input', () => {
vm = mountComponent(Applications, {
applications: {
- helm: { title: 'Helm Tiller', status: 'installed' },
+ ...APPLICATIONS_MOCK_STATE,
ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
- cert_manager: { title: 'Cert-Manager' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
- knative: { title: 'Knative', status: 'installed', hostname: '' },
},
});
@@ -282,15 +257,7 @@ describe('Applications', () => {
describe('without ingress installed', () => {
beforeEach(() => {
vm = mountComponent(Applications, {
- applications: {
- helm: { title: 'Helm Tiller' },
- ingress: { title: 'Ingress' },
- cert_manager: { title: 'Cert-Manager' },
- runner: { title: 'GitLab Runner' },
- prometheus: { title: 'Prometheus' },
- jupyter: { title: 'JupyterHub', status: 'not_installable' },
- knative: { title: 'Knative' },
- },
+ applications: APPLICATIONS_MOCK_STATE,
});
});
@@ -310,4 +277,51 @@ describe('Applications', () => {
});
});
});
+
+ describe('Knative application', () => {
+ const propsData = {
+ applications: {
+ ...APPLICATIONS_MOCK_STATE,
+ knative: {
+ title: 'Knative',
+ hostname: 'example.com',
+ status: 'installed',
+ externalIp: '1.1.1.1',
+ installed: true,
+ },
+ },
+ };
+ const newHostname = 'newhostname.com';
+ let wrapper;
+ let knativeDomainEditor;
+
+ beforeEach(() => {
+ wrapper = shallowMount(Applications, { propsData });
+ jest.spyOn(eventHub, '$emit');
+
+ knativeDomainEditor = wrapper.find(KnativeDomainEditor);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
+ knativeDomainEditor.vm.$emit('save', newHostname);
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
+ id: 'knative',
+ params: { hostname: newHostname },
+ });
+ });
+
+ it('emits setKnativeHostname event when knative domain editor emits change event', () => {
+ wrapper.find(KnativeDomainEditor).vm.$emit('set', newHostname);
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeHostname', {
+ id: 'knative',
+ hostname: newHostname,
+ });
+ });
+ });
});
diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js
new file mode 100644
index 00000000000..242b5701f8b
--- /dev/null
+++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js
@@ -0,0 +1,141 @@
+import { shallowMount } from '@vue/test-utils';
+import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { UPDATING } = APPLICATION_STATUS;
+
+describe('KnativeDomainEditor', () => {
+ let wrapper;
+ let knative;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(KnativeDomainEditor, {
+ propsData: { ...props },
+ });
+ };
+
+ beforeEach(() => {
+ knative = {
+ title: 'Knative',
+ hostname: 'example.com',
+ installed: true,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('knative has an assigned IP address', () => {
+ beforeEach(() => {
+ knative.externalIp = '1.1.1.1';
+ createComponent({ knative });
+ });
+
+ it('renders ip address with a clipboard button', () => {
+ expect(wrapper.find('.js-knative-endpoint').exists()).toBe(true);
+ expect(wrapper.find('.js-knative-endpoint').element.value).toEqual(knative.externalIp);
+ });
+
+ it('displays ip address clipboard button', () => {
+ expect(wrapper.find('.js-knative-endpoint-clipboard-btn').attributes('text')).toEqual(
+ knative.externalIp,
+ );
+ });
+
+ it('renders domain & allows editing', () => {
+ const domainNameInput = wrapper.find('.js-knative-domainname');
+
+ expect(domainNameInput.element.value).toEqual(knative.hostname);
+ expect(domainNameInput.attributes('readonly')).toBeFalsy();
+ });
+
+ it('renders an update/save Knative domain button', () => {
+ expect(wrapper.find('.js-knative-save-domain-button').exists()).toBe(true);
+ });
+ });
+
+ describe('knative without ip address', () => {
+ beforeEach(() => {
+ knative.externalIp = null;
+ createComponent({ knative });
+ });
+
+ it('renders an input text with a loading icon', () => {
+ expect(wrapper.find('.js-knative-ip-loading-icon').exists()).toBe(true);
+ });
+
+ it('renders message indicating there is not IP address assigned', () => {
+ expect(wrapper.find('.js-no-knative-endpoint-message').exists()).toBe(true);
+ });
+ });
+
+ describe('clicking save changes button', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('triggers save event and pass current knative hostname', () => {
+ wrapper.find(LoadingButton).vm.$emit('click');
+ expect(wrapper.emitted('save')[0]).toEqual([knative.hostname]);
+ });
+ });
+
+ describe('when knative domain name was saved successfully', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('displays toast indicating a successful update', () => {
+ wrapper.vm.$toast = { show: jest.fn() };
+ wrapper.setProps({ knative: Object.assign({ updateSuccessful: true }, knative) });
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(
+ 'Knative domain name was updated successfully.',
+ );
+ });
+ });
+ });
+
+ describe('when knative domain name input changes', () => {
+ it('emits "set" event with updated domain name', () => {
+ const newHostname = 'newhostname.com';
+
+ wrapper.setData({ knativeHostname: newHostname });
+
+ expect(wrapper.emitted('set')[0]).toEqual([newHostname]);
+ });
+ });
+
+ describe('when updating knative domain name failed', () => {
+ beforeEach(() => {
+ createComponent({ knative });
+ });
+
+ it('displays an error banner indicating the operation failure', () => {
+ wrapper.setProps({ knative: { updateFailed: true, ...knative } });
+
+ expect(wrapper.find('.js-cluster-knative-domain-name-failure-message').exists()).toBe(true);
+ });
+ });
+
+ describe(`when knative status is ${UPDATING}`, () => {
+ beforeEach(() => {
+ createComponent({ knative: { status: UPDATING, ...knative } });
+ });
+
+ it('renders loading spinner in save button', () => {
+ expect(wrapper.find(LoadingButton).props('loading')).toBe(true);
+ });
+
+ it('renders disabled save button', () => {
+ expect(wrapper.find(LoadingButton).props('disabled')).toBe(true);
+ });
+
+ it('renders save button with "Saving" label', () => {
+ expect(wrapper.find(LoadingButton).props('label')).toBe('Saving');
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/uninstall_application_button_spec.js b/spec/frontend/clusters/components/uninstall_application_button_spec.js
new file mode 100644
index 00000000000..9f9397d4d41
--- /dev/null
+++ b/spec/frontend/clusters/components/uninstall_application_button_spec.js
@@ -0,0 +1,32 @@
+import { shallowMount } from '@vue/test-utils';
+import UninstallApplicationButton from '~/clusters/components/uninstall_application_button.vue';
+import LoadingButton from '~/vue_shared/components/loading_button.vue';
+import { APPLICATION_STATUS } from '~/clusters/constants';
+
+const { INSTALLED, UPDATING, UNINSTALLING } = APPLICATION_STATUS;
+
+describe('UninstallApplicationButton', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(UninstallApplicationButton, {
+ propsData: { ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ status | loading | disabled | label
+ ${INSTALLED} | ${false} | ${false} | ${'Uninstall'}
+ ${UPDATING} | ${false} | ${true} | ${'Uninstall'}
+ ${UNINSTALLING} | ${true} | ${true} | ${'Uninstalling'}
+ `('when app status is $status', ({ loading, disabled, status, label }) => {
+ it(`renders a loading=${loading}, disabled=${disabled} button with label="${label}"`, () => {
+ createComponent({ status });
+ expect(wrapper.find(LoadingButton).props()).toMatchObject({ loading, disabled, label });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
new file mode 100644
index 00000000000..04808864fc0
--- /dev/null
+++ b/spec/frontend/clusters/components/uninstall_application_confirmation_modal_spec.js
@@ -0,0 +1,56 @@
+import { shallowMount } from '@vue/test-utils';
+import UninstallApplicationConfirmationModal from '~/clusters/components/uninstall_application_confirmation_modal.vue';
+import { GlModal } from '@gitlab/ui';
+import { INGRESS } from '~/clusters/constants';
+
+describe('UninstallApplicationConfirmationModal', () => {
+ let wrapper;
+ const appTitle = 'Ingress';
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(UninstallApplicationConfirmationModal, {
+ propsData: { ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ createComponent({ application: INGRESS, applicationTitle: appTitle });
+ });
+
+ it(`renders a modal with a title "Uninstall ${appTitle}"`, () => {
+ expect(wrapper.find(GlModal).attributes('title')).toEqual(`Uninstall ${appTitle}`);
+ });
+
+ it(`renders a modal with an ok button labeled "Uninstall ${appTitle}"`, () => {
+ expect(wrapper.find(GlModal).attributes('ok-title')).toEqual(`Uninstall ${appTitle}`);
+ });
+
+ describe('when ok button is clicked', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm, 'trackUninstallButtonClick');
+ wrapper.find(GlModal).vm.$emit('ok');
+ });
+
+ it('emits confirm event', () => {
+ expect(wrapper.emitted('confirm')).toBeTruthy();
+ });
+
+ it('calls track uninstall button click mixin', () => {
+ expect(wrapper.vm.trackUninstallButtonClick).toHaveBeenCalledWith(INGRESS);
+ });
+ });
+
+ it('displays a warning text indicating the app will be uninstalled', () => {
+ expect(wrapper.text()).toContain(`You are about to uninstall ${appTitle} from your cluster.`);
+ });
+
+ it('displays a custom warning text depending on the application', () => {
+ expect(wrapper.text()).toContain(
+ `The associated load balancer and IP will be deleted and cannot be restored.`,
+ );
+ });
+});
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
new file mode 100644
index 00000000000..c146ef79be7
--- /dev/null
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -0,0 +1,162 @@
+import transitionApplicationState from '~/clusters/services/application_state_machine';
+import {
+ APPLICATION_STATUS,
+ UNINSTALL_EVENT,
+ UPDATE_EVENT,
+ INSTALL_EVENT,
+} from '~/clusters/constants';
+
+const {
+ NO_STATUS,
+ SCHEDULED,
+ NOT_INSTALLABLE,
+ INSTALLABLE,
+ INSTALLING,
+ INSTALLED,
+ ERROR,
+ UPDATING,
+ UPDATED,
+ UPDATE_ERRORED,
+ UNINSTALLING,
+ UNINSTALL_ERRORED,
+} = APPLICATION_STATUS;
+
+const NO_EFFECTS = 'no effects';
+
+describe('applicationStateMachine', () => {
+ const noEffectsToEmptyObject = effects => (typeof effects === 'string' ? {} : effects);
+
+ describe(`current state is ${NO_STATUS}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${SCHEDULED} | ${NO_EFFECTS}
+ ${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ ${INSTALLING} | ${INSTALLING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ ${UPDATING} | ${UPDATING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATED} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ ${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS}
+ ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NO_STATUS,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${NOT_INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLABLE} | ${INSTALLABLE} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: NOT_INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLABLE}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLABLE,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
+
+ describe(`current state is ${INSTALLED}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
+ ${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: INSTALLED,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+
+ describe(`current state is ${UPDATING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${UPDATED} | ${{ updateSuccessful: true }}
+ ${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: UPDATING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+
+ describe(`current state is ${UNINSTALLING}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLABLE} | ${INSTALLABLE} | ${{ uninstallSuccessful: true }}
+ ${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: UNINSTALLING,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...effects,
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index 3c3d9977ffb..41ad398e924 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -11,38 +11,46 @@ const CLUSTERS_MOCK_DATA = {
name: 'helm',
status: APPLICATION_STATUS.INSTALLABLE,
status_reason: null,
+ can_uninstall: false,
},
{
name: 'ingress',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
external_ip: null,
+ external_hostname: null,
+ can_uninstall: false,
},
{
name: 'runner',
status: APPLICATION_STATUS.INSTALLING,
status_reason: null,
+ can_uninstall: false,
},
{
name: 'prometheus',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
+ can_uninstall: false,
},
{
name: 'jupyter',
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
+ can_uninstall: false,
},
{
name: 'knative',
status: APPLICATION_STATUS.INSTALLING,
status_reason: 'Cannot connect',
+ can_uninstall: false,
},
{
name: 'cert_manager',
status: APPLICATION_STATUS.ERROR,
status_reason: 'Cannot connect',
email: 'test@example.com',
+ can_uninstall: false,
},
],
},
@@ -62,6 +70,7 @@ const CLUSTERS_MOCK_DATA = {
status: APPLICATION_STATUS.INSTALLED,
status_reason: 'Cannot connect',
external_ip: '1.1.1.1',
+ external_hostname: null,
},
{
name: 'runner',
@@ -111,8 +120,17 @@ const DEFAULT_APPLICATION_STATE = {
description: 'Some description about this interesting application!',
status: null,
statusReason: null,
- requestStatus: null,
requestReason: null,
};
-export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE };
+const APPLICATIONS_MOCK_STATE = {
+ helm: { title: 'Helm Tiller', status: 'installable' },
+ ingress: { title: 'Ingress', status: 'installable' },
+ cert_manager: { title: 'Cert-Manager', status: 'installable' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ jupyter: { title: 'JupyterHub', status: 'installable', hostname: '' },
+ knative: { title: 'Knative ', status: 'installable', hostname: '' },
+};
+
+export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE };
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index 37a4d6614f6..f2cc413512d 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -1,5 +1,5 @@
import ClustersStore from '~/clusters/stores/clusters_store';
-import { APPLICATION_STATUS } from '~/clusters/constants';
+import { APPLICATION_INSTALLED_STATUSES, APPLICATION_STATUS, RUNNER } from '~/clusters/constants';
import { CLUSTERS_MOCK_DATA } from '../services/mock_data';
describe('Clusters Store', () => {
@@ -32,15 +32,6 @@ describe('Clusters Store', () => {
});
describe('updateAppProperty', () => {
- it('should store new request status', () => {
- expect(store.state.applications.helm.requestStatus).toEqual(null);
-
- const newStatus = APPLICATION_STATUS.INSTALLING;
- store.updateAppProperty('helm', 'requestStatus', newStatus);
-
- expect(store.state.applications.helm.requestStatus).toEqual(newStatus);
- });
-
it('should store new request reason', () => {
expect(store.state.applications.helm.requestReason).toEqual(null);
@@ -68,63 +59,112 @@ describe('Clusters Store', () => {
title: 'Helm Tiller',
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
- requestStatus: null,
requestReason: null,
+ installed: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
ingress: {
title: 'Ingress',
- status: mockResponseData.applications[1].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[1].status_reason,
- requestStatus: null,
requestReason: null,
externalIp: null,
+ externalHostname: null,
+ installed: false,
+ installFailed: true,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
runner: {
title: 'GitLab Runner',
status: mockResponseData.applications[2].status,
statusReason: mockResponseData.applications[2].status_reason,
- requestStatus: null,
requestReason: null,
version: mockResponseData.applications[2].version,
- upgradeAvailable: mockResponseData.applications[2].update_available,
+ updateAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/charts/gitlab-runner',
+ installed: false,
+ installFailed: false,
+ updateFailed: false,
+ updateSuccessful: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
prometheus: {
title: 'Prometheus',
- status: mockResponseData.applications[3].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason,
- requestStatus: null,
requestReason: null,
+ installed: false,
+ installFailed: true,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason,
- requestStatus: null,
requestReason: null,
hostname: '',
+ installed: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
knative: {
title: 'Knative',
status: mockResponseData.applications[5].status,
statusReason: mockResponseData.applications[5].status_reason,
- requestStatus: null,
requestReason: null,
hostname: null,
+ isEditingHostName: false,
externalIp: null,
+ externalHostname: null,
+ installed: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
+ updateSuccessful: false,
+ updateFailed: false,
},
cert_manager: {
title: 'Cert-Manager',
- status: mockResponseData.applications[6].status,
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installFailed: true,
statusReason: mockResponseData.applications[6].status_reason,
- requestStatus: null,
requestReason: null,
email: mockResponseData.applications[6].email,
+ installed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
},
},
});
});
+ describe.each(APPLICATION_INSTALLED_STATUSES)('given the current app status is %s', status => {
+ it('marks application as installed', () => {
+ const mockResponseData =
+ CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
+ const runnerAppIndex = 2;
+
+ mockResponseData.applications[runnerAppIndex].status = status;
+
+ store.updateStateFromServer(mockResponseData);
+
+ expect(store.state.applications[RUNNER].installed).toBe(true);
+ });
+ });
+
it('sets default hostname for jupyter when ingress has a ip address', () => {
const mockResponseData =
CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
diff --git a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js b/spec/frontend/cycle_analytics/limit_warning_component_spec.js
index 13e9fe00a00..13e9fe00a00 100644
--- a/spec/javascripts/cycle_analytics/limit_warning_component_spec.js
+++ b/spec/frontend/cycle_analytics/limit_warning_component_spec.js
diff --git a/spec/javascripts/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js
index 984b3026209..984b3026209 100644
--- a/spec/javascripts/diffs/components/diff_stats_spec.js
+++ b/spec/frontend/diffs/components/diff_stats_spec.js
diff --git a/spec/frontend/diffs/components/edit_button_spec.js b/spec/frontend/diffs/components/edit_button_spec.js
new file mode 100644
index 00000000000..ccdae4cb312
--- /dev/null
+++ b/spec/frontend/diffs/components/edit_button_spec.js
@@ -0,0 +1,61 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import EditButton from '~/diffs/components/edit_button.vue';
+
+const localVue = createLocalVue();
+const editPath = 'test-path';
+
+describe('EditButton', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(EditButton, {
+ localVue,
+ sync: false,
+ propsData: { ...props },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has correct href attribute', () => {
+ createComponent({
+ editPath,
+ canCurrentUserFork: false,
+ });
+
+ expect(wrapper.attributes('href')).toBe(editPath);
+ });
+
+ it('emits a show fork message event if current user can fork', () => {
+ createComponent({
+ editPath,
+ canCurrentUserFork: true,
+ });
+ wrapper.trigger('click');
+
+ expect(wrapper.emitted('showForkMessage')).toBeTruthy();
+ });
+
+ it('doesnt emit a show fork message event if current user cannot fork', () => {
+ createComponent({
+ editPath,
+ canCurrentUserFork: false,
+ });
+ wrapper.trigger('click');
+
+ expect(wrapper.emitted('showForkMessage')).toBeFalsy();
+ });
+
+ it('doesnt emit a show fork message event if current user can modify blob', () => {
+ createComponent({
+ editPath,
+ canCurrentUserFork: true,
+ canModifyBlob: true,
+ });
+ wrapper.trigger('click');
+
+ expect(wrapper.emitted('showForkMessage')).toBeFalsy();
+ });
+});
diff --git a/spec/frontend/diffs/components/hidden_files_warning_spec.js b/spec/frontend/diffs/components/hidden_files_warning_spec.js
new file mode 100644
index 00000000000..5bf5ddd27bd
--- /dev/null
+++ b/spec/frontend/diffs/components/hidden_files_warning_spec.js
@@ -0,0 +1,48 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
+
+const localVue = createLocalVue();
+const propsData = {
+ total: '10',
+ visible: 5,
+ plainDiffPath: 'plain-diff-path',
+ emailPatchPath: 'email-patch-path',
+};
+
+describe('HiddenFilesWarning', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(HiddenFilesWarning, {
+ localVue,
+ sync: false,
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('has a correct plain diff URL', () => {
+ const plainDiffLink = wrapper.findAll('a').wrappers.filter(x => x.text() === 'Plain diff')[0];
+
+ expect(plainDiffLink.attributes('href')).toBe(propsData.plainDiffPath);
+ });
+
+ it('has a correct email patch URL', () => {
+ const emailPatchLink = wrapper.findAll('a').wrappers.filter(x => x.text() === 'Email patch')[0];
+
+ expect(emailPatchLink.attributes('href')).toBe(propsData.emailPatchPath);
+ });
+
+ it('has a correct visible/total files text', () => {
+ const filesText = wrapper.find('strong');
+
+ expect(filesText.text()).toBe('5 of 10');
+ });
+});
diff --git a/spec/javascripts/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js
index e45d34bf9d5..e45d34bf9d5 100644
--- a/spec/javascripts/diffs/components/no_changes_spec.js
+++ b/spec/frontend/diffs/components/no_changes_spec.js
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
new file mode 100644
index 00000000000..a8c8688441d
--- /dev/null
+++ b/spec/frontend/environment.js
@@ -0,0 +1,67 @@
+/* eslint-disable import/no-commonjs */
+
+const { ErrorWithStack } = require('jest-util');
+const JSDOMEnvironment = require('jest-environment-jsdom');
+
+class CustomEnvironment extends JSDOMEnvironment {
+ constructor(config, context) {
+ super(config, context);
+
+ Object.assign(context.console, {
+ error(...args) {
+ throw new ErrorWithStack(
+ `Unexpected call of console.error() with:\n\n${args.join(', ')}`,
+ this.error,
+ );
+ },
+
+ warn(...args) {
+ throw new ErrorWithStack(
+ `Unexpected call of console.warn() with:\n\n${args.join(', ')}`,
+ this.warn,
+ );
+ },
+ });
+
+ const { testEnvironmentOptions } = config;
+ const { IS_EE } = testEnvironmentOptions;
+ this.global.gon = {
+ ee: IS_EE,
+ };
+
+ this.rejectedPromises = [];
+
+ this.global.promiseRejectionHandler = error => {
+ this.rejectedPromises.push(error);
+ };
+
+ this.global.fixturesBasePath = `${process.cwd()}/${
+ IS_EE ? 'ee/' : ''
+ }spec/javascripts/fixtures`;
+
+ // Not yet supported by JSDOM: https://github.com/jsdom/jsdom/issues/317
+ this.global.document.createRange = () => ({
+ setStart: () => {},
+ setEnd: () => {},
+ commonAncestorContainer: {
+ nodeName: 'BODY',
+ ownerDocument: this.global.document,
+ },
+ });
+ }
+
+ async teardown() {
+ await new Promise(setImmediate);
+
+ if (this.rejectedPromises.length > 0) {
+ throw new ErrorWithStack(
+ `Unhandled Promise rejections: ${this.rejectedPromises.join(', ')}`,
+ this.teardown,
+ );
+ }
+
+ await super.teardown();
+ }
+}
+
+module.exports = CustomEnvironment;
diff --git a/spec/javascripts/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 08bbb390993..67e5dc399ac 100644
--- a/spec/javascripts/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -1,7 +1,7 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
-import { GlButton, GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlLoadingIcon, GlTable, GlLink } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -9,6 +9,7 @@ localVue.use(Vuex);
describe('ErrorTrackingList', () => {
let store;
let wrapper;
+ let actions;
function mountComponent({ errorTrackingEnabled = true } = {}) {
wrapper = shallowMount(ErrorTrackingList, {
@@ -20,12 +21,17 @@ describe('ErrorTrackingList', () => {
errorTrackingEnabled,
illustrationPath: 'illustration/path',
},
+ stubs: {
+ 'gl-link': GlLink,
+ },
});
}
beforeEach(() => {
- const actions = {
+ actions = {
getErrorList: () => {},
+ startPolling: () => {},
+ restartPolling: jest.fn().mockName('restartPolling'),
};
const state = {
@@ -83,6 +89,18 @@ describe('ErrorTrackingList', () => {
expect(wrapper.find(GlTable).exists()).toBeTruthy();
expect(wrapper.find(GlButton).exists()).toBeTruthy();
});
+
+ it('shows a message prompting to refresh', () => {
+ const refreshLink = wrapper.vm.$refs.empty.querySelector('a');
+
+ expect(refreshLink.textContent.trim()).toContain('Check again');
+ });
+
+ it('restarts polling', () => {
+ wrapper.find('.js-try-again').trigger('click');
+
+ expect(actions.restartPolling).toHaveBeenCalled();
+ });
});
describe('error tracking feature disabled', () => {
diff --git a/spec/javascripts/error_tracking/store/mutation_spec.js b/spec/frontend/error_tracking/store/mutation_spec.js
index 8117104bdbc..8117104bdbc 100644
--- a/spec/javascripts/error_tracking/store/mutation_spec.js
+++ b/spec/frontend/error_tracking/store/mutation_spec.js
diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/frontend/filtered_search/filtered_search_token_keys_spec.js
index d1fea18dea8..d1fea18dea8 100644
--- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_token_keys_spec.js
diff --git a/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js
index ea7c146fa4f..0e62bc94517 100644
--- a/spec/javascripts/filtered_search/services/recent_searches_service_error_spec.js
+++ b/spec/frontend/filtered_search/services/recent_searches_service_error_spec.js
@@ -8,7 +8,7 @@ describe('RecentSearchesServiceError', () => {
});
it('instantiates an instance of RecentSearchesServiceError and not an Error', () => {
- expect(recentSearchesServiceError).toEqual(jasmine.any(RecentSearchesServiceError));
+ expect(recentSearchesServiceError).toEqual(expect.any(RecentSearchesServiceError));
expect(recentSearchesServiceError.name).toBe('RecentSearchesServiceError');
});
diff --git a/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js b/spec/frontend/filtered_search/stores/recent_searches_store_spec.js
index 56bb82ae941..56bb82ae941 100644
--- a/spec/javascripts/filtered_search/stores/recent_searches_store_spec.js
+++ b/spec/frontend/filtered_search/stores/recent_searches_store_spec.js
diff --git a/spec/javascripts/frequent_items/store/getters_spec.js b/spec/frontend/frequent_items/store/getters_spec.js
index 1cd12eb6832..1cd12eb6832 100644
--- a/spec/javascripts/frequent_items/store/getters_spec.js
+++ b/spec/frontend/frequent_items/store/getters_spec.js
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index c7008c780d6..8af49fd47a2 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -1,10 +1,15 @@
/* eslint no-param-reassign: "off" */
import $ from 'jquery';
-import GfmAutoComplete from '~/gfm_auto_complete';
+import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
-import 'vendor/jquery.caret';
-import 'vendor/jquery.atwho';
+import 'jquery.caret';
+import 'at.js';
+
+import { TEST_HOST } from 'helpers/test_constants';
+import { getJSONFixture } from 'helpers/fixtures';
+
+const labelsFixture = getJSONFixture('autocomplete_sources/labels.json');
describe('GfmAutoComplete', () => {
const gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
@@ -12,11 +17,12 @@ describe('GfmAutoComplete', () => {
});
let atwhoInstance;
- let items;
let sorterValue;
describe('DefaultOptions.sorter', () => {
describe('assets loading', () => {
+ let items;
+
beforeEach(() => {
jest.spyOn(GfmAutoComplete, 'isLoading').mockReturnValue(true);
@@ -61,7 +67,7 @@ describe('GfmAutoComplete', () => {
atwhoInstance = { setting: {} };
const query = 'query';
- items = [];
+ const items = [];
const searchKey = 'searchKey';
gfmAutoCompleteCallbacks.sorter.call(atwhoInstance, query, items, searchKey);
@@ -85,7 +91,7 @@ describe('GfmAutoComplete', () => {
});
it('should quote if value contains any non-alphanumeric characters', () => {
- expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label\\-20"');
+ expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
});
@@ -93,12 +99,21 @@ describe('GfmAutoComplete', () => {
expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
});
- it('should escape Markdown emphasis characters, except in the first character', () => {
- expect(beforeInsert(atwhoInstance, '@_group')).toEqual('@\\_group');
- expect(beforeInsert(atwhoInstance, '~_bug')).toEqual('~\\_bug');
+ it('escapes Markdown strikethroughs when needed', () => {
+ expect(beforeInsert(atwhoInstance, '~a~bug')).toEqual('~"a~bug"');
+ expect(beforeInsert(atwhoInstance, '~a~~bug~~')).toEqual('~"a\\~~bug\\~~"');
+ });
+
+ it('escapes Markdown emphasis when needed', () => {
+ expect(beforeInsert(atwhoInstance, '~a_bug_')).toEqual('~a_bug\\_');
+ expect(beforeInsert(atwhoInstance, '~a _bug_')).toEqual('~"a \\_bug\\_"');
+ expect(beforeInsert(atwhoInstance, '~a*bug*')).toEqual('~"a\\*bug\\*"');
+ expect(beforeInsert(atwhoInstance, '~a *bug*')).toEqual('~"a \\*bug\\*"');
+ });
+
+ it('escapes Markdown code spans when needed', () => {
+ expect(beforeInsert(atwhoInstance, '~a`bug`')).toEqual('~"a\\`bug\\`"');
expect(beforeInsert(atwhoInstance, '~a `bug`')).toEqual('~"a \\`bug\\`"');
- expect(beforeInsert(atwhoInstance, '~a ~bug')).toEqual('~"a \\~bug"');
- expect(beforeInsert(atwhoInstance, '~a **bug')).toEqual('~"a \\*\\*bug"');
});
});
@@ -191,6 +206,38 @@ describe('GfmAutoComplete', () => {
});
});
+ describe('DefaultOptions.highlighter', () => {
+ beforeEach(() => {
+ atwhoInstance = { setting: {} };
+ });
+
+ it('should return li if no query is given', () => {
+ const liTag = '<li></li>';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag);
+
+ expect(highlightedTag).toEqual(liTag);
+ });
+
+ it('should highlight search query in li element', () => {
+ const liTag = '<li><img src="" />string</li>';
+ const query = 's';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag, query);
+
+ expect(highlightedTag).toEqual('<li><img src="" /> <strong>s</strong>tring </li>');
+ });
+
+ it('should highlight search query with special char in li element', () => {
+ const liTag = '<li><img src="" />te.st</li>';
+ const query = '.';
+
+ const highlightedTag = gfmAutoCompleteCallbacks.highlighter.call(atwhoInstance, liTag, query);
+
+ expect(highlightedTag).toEqual('<li><img src="" /> te<strong>.</strong>st </li>');
+ });
+ });
+
describe('isLoading', () => {
it('should be true with loading data object item', () => {
expect(GfmAutoComplete.isLoading({ name: 'loading' })).toBe(true);
@@ -250,4 +297,90 @@ describe('GfmAutoComplete', () => {
).toBe('<li><small>grp/proj#5</small> Some Issue</li>');
});
});
+
+ describe('labels', () => {
+ const dataSources = {
+ labels: `${TEST_HOST}/autocomplete_sources/labels`,
+ };
+
+ const allLabels = labelsFixture;
+ const assignedLabels = allLabels.filter(label => label.set);
+ const unassignedLabels = allLabels.filter(label => !label.set);
+
+ let autocomplete;
+ let $textarea;
+
+ beforeEach(() => {
+ autocomplete = new GfmAutoComplete(dataSources);
+ $textarea = $('<textarea></textarea>');
+ autocomplete.setup($textarea, { labels: true });
+ });
+
+ afterEach(() => {
+ autocomplete.destroy();
+ });
+
+ const triggerDropdown = text => {
+ $textarea
+ .trigger('focus')
+ .val(text)
+ .caret('pos', -1);
+ $textarea.trigger('keyup');
+
+ return new Promise(window.requestAnimationFrame);
+ };
+
+ const getDropdownItems = () => {
+ const dropdown = document.getElementById('at-view-labels');
+ const items = dropdown.getElementsByTagName('li');
+ return [].map.call(items, item => item.textContent.trim());
+ };
+
+ const expectLabels = ({ input, output }) =>
+ triggerDropdown(input).then(() => {
+ expect(getDropdownItems()).toEqual(output.map(label => label.title));
+ });
+
+ describe('with no labels assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['~'] = [...unassignedLabels];
+ });
+
+ it.each`
+ input | output
+ ${'~'} | ${unassignedLabels}
+ ${'/label ~'} | ${unassignedLabels}
+ ${'/relabel ~'} | ${unassignedLabels}
+ ${'/unlabel ~'} | ${[]}
+ `('$input shows $output.length labels', expectLabels);
+ });
+
+ describe('with some labels assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['~'] = allLabels;
+ });
+
+ it.each`
+ input | output
+ ${'~'} | ${allLabels}
+ ${'/label ~'} | ${unassignedLabels}
+ ${'/relabel ~'} | ${allLabels}
+ ${'/unlabel ~'} | ${assignedLabels}
+ `('$input shows $output.length labels', expectLabels);
+ });
+
+ describe('with all labels assigned', () => {
+ beforeEach(() => {
+ autocomplete.cachedData['~'] = [...assignedLabels];
+ });
+
+ it.each`
+ input | output
+ ${'~'} | ${assignedLabels}
+ ${'/label ~'} | ${[]}
+ ${'/relabel ~'} | ${assignedLabels}
+ ${'/unlabel ~'} | ${assignedLabels}
+ `('$input shows $output.length labels', expectLabels);
+ });
+ });
});
diff --git a/spec/frontend/helpers/class_spec_helper.js b/spec/frontend/helpers/class_spec_helper.js
new file mode 100644
index 00000000000..7a60d33b471
--- /dev/null
+++ b/spec/frontend/helpers/class_spec_helper.js
@@ -0,0 +1,9 @@
+export default class ClassSpecHelper {
+ static itShouldBeAStaticMethod(base, method) {
+ return it('should be a static method', () => {
+ expect(Object.prototype.hasOwnProperty.call(base, method)).toBeTruthy();
+ });
+ }
+}
+
+window.ClassSpecHelper = ClassSpecHelper;
diff --git a/spec/frontend/helpers/fixtures.js b/spec/frontend/helpers/fixtures.js
new file mode 100644
index 00000000000..b77bcd6266e
--- /dev/null
+++ b/spec/frontend/helpers/fixtures.js
@@ -0,0 +1,34 @@
+import fs from 'fs';
+import path from 'path';
+
+import { ErrorWithStack } from 'jest-util';
+
+export function getFixture(relativePath) {
+ const absolutePath = path.join(global.fixturesBasePath, relativePath);
+ if (!fs.existsSync(absolutePath)) {
+ throw new ErrorWithStack(
+ `Fixture file ${relativePath} does not exist.
+
+Did you run bin/rake karma:fixtures?`,
+ getFixture,
+ );
+ }
+
+ return fs.readFileSync(absolutePath, 'utf8');
+}
+
+export const getJSONFixture = relativePath => JSON.parse(getFixture(relativePath));
+
+export const resetHTMLFixture = () => {
+ document.body.textContent = '';
+};
+
+export const setHTMLFixture = (htmlContent, resetHook = afterEach) => {
+ document.body.outerHTML = htmlContent;
+ resetHook(resetHTMLFixture);
+};
+
+export const loadHTMLFixture = (relativePath, resetHook = afterEach) => {
+ const fileContent = getFixture(relativePath);
+ setHTMLFixture(fileContent, resetHook);
+};
diff --git a/spec/frontend/helpers/jest_helpers.js b/spec/frontend/helpers/jest_helpers.js
new file mode 100644
index 00000000000..4a150be9935
--- /dev/null
+++ b/spec/frontend/helpers/jest_helpers.js
@@ -0,0 +1,24 @@
+/* eslint-disable import/prefer-default-export */
+
+/*
+@module
+
+This method provides convenience functions to help migrating from Karma/Jasmine to Jest.
+
+Try not to use these in new tests - this module is provided primarily for convenience of migrating tests.
+ */
+
+/**
+ * Creates a plain JS object pre-populated with Jest spy functions. Useful for making simple mocks classes.
+ *
+ * @see https://jasmine.github.io/2.0/introduction.html#section-Spies:_%3Ccode%3EcreateSpyObj%3C/code%3E
+ * @param {string} baseName Human-readable name of the object. This is used for reporting purposes.
+ * @param methods {string[]} List of method names that will be added to the spy object.
+ */
+export function createSpyObj(baseName, methods) {
+ const obj = {};
+ methods.forEach(method => {
+ obj[method] = jest.fn().mockName(`${baseName}#${method}`);
+ });
+ return obj;
+}
diff --git a/spec/frontend/helpers/jquery.js b/spec/frontend/helpers/jquery.js
new file mode 100644
index 00000000000..6421a592c0c
--- /dev/null
+++ b/spec/frontend/helpers/jquery.js
@@ -0,0 +1,6 @@
+import $ from 'jquery';
+
+global.$ = $;
+global.jQuery = $;
+
+export default $;
diff --git a/spec/frontend/helpers/local_storage_helper.js b/spec/frontend/helpers/local_storage_helper.js
new file mode 100644
index 00000000000..48e66b11767
--- /dev/null
+++ b/spec/frontend/helpers/local_storage_helper.js
@@ -0,0 +1,41 @@
+/**
+ * Manage the instance of a custom `window.localStorage`
+ *
+ * This only encapsulates the setup / teardown logic so that it can easily be
+ * reused with different implementations (i.e. a spy or a [fake][1])
+ *
+ * [1]: https://stackoverflow.com/a/41434763/1708147
+ *
+ * @param {() => any} fn Function that returns the object to use for localStorage
+ */
+const useLocalStorage = fn => {
+ const origLocalStorage = window.localStorage;
+ let currentLocalStorage;
+
+ Object.defineProperty(window, 'localStorage', {
+ get: () => currentLocalStorage,
+ });
+
+ beforeEach(() => {
+ currentLocalStorage = fn();
+ });
+
+ afterEach(() => {
+ currentLocalStorage = origLocalStorage;
+ });
+};
+
+/**
+ * Create an object with the localStorage interface but `jest.fn()` implementations.
+ */
+export const createLocalStorageSpy = () => ({
+ clear: jest.fn(),
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+ removeItem: jest.fn(),
+});
+
+/**
+ * Before each test, overwrite `window.localStorage` with a spy implementation.
+ */
+export const useLocalStorageSpy = () => useLocalStorage(createLocalStorageSpy);
diff --git a/spec/frontend/helpers/locale_helper.js b/spec/frontend/helpers/locale_helper.js
new file mode 100644
index 00000000000..80047b06003
--- /dev/null
+++ b/spec/frontend/helpers/locale_helper.js
@@ -0,0 +1,11 @@
+/* eslint-disable import/prefer-default-export */
+
+export const setLanguage = languageCode => {
+ const htmlElement = document.querySelector('html');
+
+ if (languageCode) {
+ htmlElement.setAttribute('lang', languageCode);
+ } else {
+ htmlElement.removeAttribute('lang');
+ }
+};
diff --git a/spec/frontend/helpers/monitor_helper_spec.js b/spec/frontend/helpers/monitor_helper_spec.js
new file mode 100644
index 00000000000..2e8bff298c4
--- /dev/null
+++ b/spec/frontend/helpers/monitor_helper_spec.js
@@ -0,0 +1,45 @@
+import * as monitorHelper from '~/helpers/monitor_helper';
+
+describe('monitor helper', () => {
+ const defaultConfig = { default: true, name: 'default name' };
+ const name = 'data name';
+ const series = [[1, 1], [2, 2], [3, 3]];
+ const data = ({ metric = { default_name: name }, values = series } = {}) => [{ metric, values }];
+
+ describe('makeDataSeries', () => {
+ const expectedDataSeries = [
+ {
+ ...defaultConfig,
+ data: series,
+ },
+ ];
+
+ it('converts query results to data series', () => {
+ expect(monitorHelper.makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual(
+ expectedDataSeries,
+ );
+ });
+
+ it('returns an empty array if no query results exist', () => {
+ expect(monitorHelper.makeDataSeries([], defaultConfig)).toEqual([]);
+ });
+
+ it('handles multi-series query results', () => {
+ const expectedData = { ...expectedDataSeries[0], name: 'default name: data name' };
+
+ expect(monitorHelper.makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
+ expectedData,
+ expectedData,
+ ]);
+ });
+
+ it('excludes NaN values', () => {
+ expect(
+ monitorHelper.makeDataSeries(
+ data({ metric: {}, values: [[1, 1], [2, NaN]] }),
+ defaultConfig,
+ ),
+ ).toEqual([{ ...expectedDataSeries[0], data: [[1, 1]] }]);
+ });
+ });
+});
diff --git a/spec/frontend/helpers/scroll_into_view_promise.js b/spec/frontend/helpers/scroll_into_view_promise.js
new file mode 100644
index 00000000000..0edea2103da
--- /dev/null
+++ b/spec/frontend/helpers/scroll_into_view_promise.js
@@ -0,0 +1,28 @@
+export default function scrollIntoViewPromise(intersectionTarget, timeout = 100, maxTries = 5) {
+ return new Promise((resolve, reject) => {
+ let intersectionObserver;
+ let retry = 0;
+
+ const intervalId = setInterval(() => {
+ if (retry >= maxTries) {
+ intersectionObserver.disconnect();
+ clearInterval(intervalId);
+ reject(new Error(`Could not scroll target into viewPort within ${timeout * maxTries} ms`));
+ }
+ retry += 1;
+ intersectionTarget.scrollIntoView();
+ }, timeout);
+
+ intersectionObserver = new IntersectionObserver(entries => {
+ if (entries[0].isIntersecting) {
+ intersectionObserver.disconnect();
+ clearInterval(intervalId);
+ resolve();
+ }
+ });
+
+ intersectionObserver.observe(intersectionTarget);
+
+ intersectionTarget.scrollIntoView();
+ });
+}
diff --git a/spec/frontend/helpers/set_timeout_promise_helper.js b/spec/frontend/helpers/set_timeout_promise_helper.js
new file mode 100644
index 00000000000..47087619187
--- /dev/null
+++ b/spec/frontend/helpers/set_timeout_promise_helper.js
@@ -0,0 +1,4 @@
+export default (time = 0) =>
+ new Promise(resolve => {
+ setTimeout(resolve, time);
+ });
diff --git a/spec/javascripts/helpers/vue_component_helper.js b/spec/frontend/helpers/text_helper.js
index e0fe18e5560..e0fe18e5560 100644
--- a/spec/javascripts/helpers/vue_component_helper.js
+++ b/spec/frontend/helpers/text_helper.js
diff --git a/spec/frontend/helpers/timeout.js b/spec/frontend/helpers/timeout.js
new file mode 100644
index 00000000000..702ef0be5aa
--- /dev/null
+++ b/spec/frontend/helpers/timeout.js
@@ -0,0 +1,59 @@
+const NS_PER_SEC = 1e9;
+const NS_PER_MS = 1e6;
+const IS_DEBUGGING = process.execArgv.join(' ').includes('--inspect-brk');
+
+let testTimeoutNS;
+
+export const setTestTimeout = newTimeoutMS => {
+ const newTimeoutNS = newTimeoutMS * NS_PER_MS;
+ // never accept a smaller timeout than the default
+ if (newTimeoutNS < testTimeoutNS) {
+ return;
+ }
+
+ testTimeoutNS = newTimeoutNS;
+ jest.setTimeout(newTimeoutMS);
+};
+
+// Allows slow tests to set their own timeout.
+// Useful for tests with jQuery, which is very slow in big DOMs.
+let temporaryTimeoutNS = null;
+export const setTestTimeoutOnce = newTimeoutMS => {
+ const newTimeoutNS = newTimeoutMS * NS_PER_MS;
+ // never accept a smaller timeout than the default
+ if (newTimeoutNS < testTimeoutNS) {
+ return;
+ }
+
+ temporaryTimeoutNS = newTimeoutNS;
+};
+
+export const initializeTestTimeout = defaultTimeoutMS => {
+ setTestTimeout(defaultTimeoutMS);
+
+ let testStartTime;
+
+ // https://github.com/facebook/jest/issues/6947
+ beforeEach(() => {
+ testStartTime = process.hrtime();
+ });
+
+ afterEach(() => {
+ let timeoutNS = testTimeoutNS;
+ if (Number.isFinite(temporaryTimeoutNS)) {
+ timeoutNS = temporaryTimeoutNS;
+ temporaryTimeoutNS = null;
+ }
+
+ const [seconds, remainingNs] = process.hrtime(testStartTime);
+ const elapsedNS = seconds * NS_PER_SEC + remainingNs;
+
+ // Disable the timeout error when debugging. It is meaningless because
+ // debugging always takes longer than the test timeout.
+ if (elapsedNS > timeoutNS && !IS_DEBUGGING) {
+ throw new Error(
+ `Test took too long (${elapsedNS / NS_PER_MS}ms > ${timeoutNS / NS_PER_MS}ms)!`,
+ );
+ }
+ });
+};
diff --git a/spec/frontend/helpers/user_mock_data_helper.js b/spec/frontend/helpers/user_mock_data_helper.js
new file mode 100644
index 00000000000..6999fa1f8a1
--- /dev/null
+++ b/spec/frontend/helpers/user_mock_data_helper.js
@@ -0,0 +1,14 @@
+export default {
+ createNumberRandomUsers(numberUsers) {
+ const users = [];
+ for (let i = 0; i < numberUsers; i += 1) {
+ users.push({
+ avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: i + 1,
+ name: `GitLab User ${i}`,
+ username: `gitlab${i}`,
+ });
+ }
+ return users;
+ },
+};
diff --git a/spec/frontend/helpers/vue_mount_component_helper.js b/spec/frontend/helpers/vue_mount_component_helper.js
new file mode 100644
index 00000000000..6848c95d95d
--- /dev/null
+++ b/spec/frontend/helpers/vue_mount_component_helper.js
@@ -0,0 +1,38 @@
+import Vue from 'vue';
+
+const mountComponent = (Component, props = {}, el = null) =>
+ new Component({
+ propsData: props,
+ }).$mount(el);
+
+export const createComponentWithStore = (Component, store, propsData = {}) =>
+ new Component({
+ store,
+ propsData,
+ });
+
+export const mountComponentWithStore = (Component, { el, props, store }) =>
+ new Component({
+ store,
+ propsData: props || {},
+ }).$mount(el);
+
+export const mountComponentWithSlots = (Component, { props, slots }) => {
+ const component = new Component({
+ propsData: props || {},
+ });
+
+ component.$slots = slots;
+
+ return component.$mount();
+};
+
+/**
+ * Mount a component with the given render method.
+ *
+ * This helps with inserting slots that need to be compiled.
+ */
+export const mountComponentWithRender = (render, el = null) =>
+ mountComponent(Vue.extend({ render }), {}, el);
+
+export default mountComponent;
diff --git a/spec/frontend/helpers/vue_resource_helper.js b/spec/frontend/helpers/vue_resource_helper.js
new file mode 100644
index 00000000000..0f58af09933
--- /dev/null
+++ b/spec/frontend/helpers/vue_resource_helper.js
@@ -0,0 +1,11 @@
+// eslint-disable-next-line import/prefer-default-export
+export const headersInterceptor = (request, next) => {
+ next(response => {
+ const headers = {};
+ response.headers.forEach((value, key) => {
+ headers[key] = value;
+ });
+ // eslint-disable-next-line no-param-reassign
+ response.headers = headers;
+ });
+};
diff --git a/spec/frontend/helpers/vue_test_utils_helper.js b/spec/frontend/helpers/vue_test_utils_helper.js
new file mode 100644
index 00000000000..121e99c9783
--- /dev/null
+++ b/spec/frontend/helpers/vue_test_utils_helper.js
@@ -0,0 +1,21 @@
+/* eslint-disable import/prefer-default-export */
+
+const vNodeContainsText = (vnode, text) =>
+ (vnode.text && vnode.text.includes(text)) ||
+ (vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length);
+
+/**
+ * Determines whether a `shallowMount` Wrapper contains text
+ * within one of it's slots. This will also work on Wrappers
+ * acquired with `find()`, but only if it's parent Wrapper
+ * was shallowMounted.
+ * NOTE: Prefer checking the rendered output of a component
+ * wherever possible using something like `text()` instead.
+ * @param {Wrapper} shallowWrapper - Vue test utils wrapper (shallowMounted)
+ * @param {String} slotName
+ * @param {String} text
+ */
+export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =>
+ Boolean(
+ shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length,
+ );
diff --git a/spec/frontend/helpers/vuex_action_helper.js b/spec/frontend/helpers/vuex_action_helper.js
new file mode 100644
index 00000000000..88652202a8e
--- /dev/null
+++ b/spec/frontend/helpers/vuex_action_helper.js
@@ -0,0 +1,104 @@
+const noop = () => {};
+
+/**
+ * Helper for testing action with expected mutations inspired in
+ * https://vuex.vuejs.org/en/testing.html
+ *
+ * @param {Function} action to be tested
+ * @param {Object} payload will be provided to the action
+ * @param {Object} state will be provided to the action
+ * @param {Array} [expectedMutations=[]] mutations expected to be committed
+ * @param {Array} [expectedActions=[]] actions expected to be dispatched
+ * @param {Function} [done=noop] to be executed after the tests
+ * @return {Promise}
+ *
+ * @example
+ * testAction(
+ * actions.actionName, // action
+ * { }, // mocked payload
+ * state, //state
+ * // expected mutations
+ * [
+ * { type: types.MUTATION}
+ * { type: types.MUTATION_1, payload: jasmine.any(Number)}
+ * ],
+ * // expected actions
+ * [
+ * { type: 'actionName', payload: {param: 'foobar'}},
+ * { type: 'actionName1'}
+ * ]
+ * done,
+ * );
+ *
+ * @example
+ * testAction(
+ * actions.actionName, // action
+ * { }, // mocked payload
+ * state, //state
+ * [ { type: types.MUTATION} ], // expected mutations
+ * [], // expected actions
+ * ).then(done)
+ * .catch(done.fail);
+ */
+export default (
+ action,
+ payload,
+ state,
+ expectedMutations = [],
+ expectedActions = [],
+ done = noop,
+) => {
+ const mutations = [];
+ const actions = [];
+
+ // mock commit
+ const commit = (type, mutationPayload) => {
+ const mutation = { type };
+
+ if (typeof mutationPayload !== 'undefined') {
+ mutation.payload = mutationPayload;
+ }
+
+ mutations.push(mutation);
+ };
+
+ // mock dispatch
+ const dispatch = (type, actionPayload) => {
+ const dispatchedAction = { type };
+
+ if (typeof actionPayload !== 'undefined') {
+ dispatchedAction.payload = actionPayload;
+ }
+
+ actions.push(dispatchedAction);
+ };
+
+ const validateResults = () => {
+ expect({
+ mutations,
+ actions,
+ }).toEqual({
+ mutations: expectedMutations,
+ actions: expectedActions,
+ });
+ done();
+ };
+
+ const result = action(
+ { commit, state, dispatch, rootState: state, rootGetters: state, getters: state },
+ payload,
+ );
+
+ return new Promise(resolve => {
+ setImmediate(resolve);
+ })
+ .then(() => result)
+ .catch(error => {
+ validateResults();
+ throw error;
+ })
+ .then(data => {
+ validateResults();
+ return data;
+ });
+};
diff --git a/spec/frontend/helpers/wait_for_attribute_change.js b/spec/frontend/helpers/wait_for_attribute_change.js
new file mode 100644
index 00000000000..8f22d569222
--- /dev/null
+++ b/spec/frontend/helpers/wait_for_attribute_change.js
@@ -0,0 +1,16 @@
+export default (domElement, attributes, timeout = 1500) =>
+ new Promise((resolve, reject) => {
+ let observer;
+ const timeoutId = setTimeout(() => {
+ observer.disconnect();
+ reject(new Error(`Could not see an attribute update within ${timeout} ms`));
+ }, timeout);
+
+ observer = new MutationObserver(() => {
+ clearTimeout(timeoutId);
+ observer.disconnect();
+ resolve();
+ });
+
+ observer.observe(domElement, { attributes: true, attributeFilter: attributes });
+ });
diff --git a/spec/frontend/helpers/wait_for_promises.js b/spec/frontend/helpers/wait_for_promises.js
new file mode 100644
index 00000000000..1d2b53fc770
--- /dev/null
+++ b/spec/frontend/helpers/wait_for_promises.js
@@ -0,0 +1 @@
+export default () => new Promise(resolve => requestAnimationFrame(resolve));
diff --git a/spec/javascripts/ide/lib/common/disposable_spec.js b/spec/frontend/ide/lib/common/disposable_spec.js
index af12ca15369..8596642eb7a 100644
--- a/spec/javascripts/ide/lib/common/disposable_spec.js
+++ b/spec/frontend/ide/lib/common/disposable_spec.js
@@ -8,7 +8,7 @@ describe('Multi-file editor library disposable class', () => {
instance = new Disposable();
disposableClass = {
- dispose: jasmine.createSpy('dispose'),
+ dispose: jest.fn().mockName('dispose'),
};
});
diff --git a/spec/frontend/ide/lib/diff/diff_spec.js b/spec/frontend/ide/lib/diff/diff_spec.js
new file mode 100644
index 00000000000..d9b088e2c12
--- /dev/null
+++ b/spec/frontend/ide/lib/diff/diff_spec.js
@@ -0,0 +1,77 @@
+import { computeDiff } from '~/ide/lib/diff/diff';
+
+describe('Multi-file editor library diff calculator', () => {
+ describe('computeDiff', () => {
+ it('returns empty array if no changes', () => {
+ const diff = computeDiff('123', '123');
+
+ expect(diff).toEqual([]);
+ });
+
+ describe('modified', () => {
+ it.each`
+ originalContent | newContent | lineNumber
+ ${'123'} | ${'1234'} | ${1}
+ ${'123\n123\n123'} | ${'123\n1234\n123'} | ${2}
+ `(
+ 'marks line $lineNumber as added and modified but not removed',
+ ({ originalContent, newContent, lineNumber }) => {
+ const diff = computeDiff(originalContent, newContent)[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeTruthy();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(lineNumber);
+ },
+ );
+ });
+
+ describe('added', () => {
+ it.each`
+ originalContent | newContent | lineNumber
+ ${'123'} | ${'123\n123'} | ${1}
+ ${'123\n123\n123'} | ${'123\n123\n1234\n123'} | ${3}
+ `(
+ 'marks line $lineNumber as added but not modified and not removed',
+ ({ originalContent, newContent, lineNumber }) => {
+ const diff = computeDiff(originalContent, newContent)[0];
+
+ expect(diff.added).toBeTruthy();
+ expect(diff.modified).toBeUndefined();
+ expect(diff.removed).toBeUndefined();
+ expect(diff.lineNumber).toBe(lineNumber);
+ },
+ );
+ });
+
+ describe('removed', () => {
+ it.each`
+ originalContent | newContent | lineNumber | modified
+ ${'123'} | ${''} | ${1} | ${undefined}
+ ${'123\n123\n123'} | ${'123\n123'} | ${2} | ${true}
+ `(
+ 'marks line $lineNumber as removed',
+ ({ originalContent, newContent, lineNumber, modified }) => {
+ const diff = computeDiff(originalContent, newContent)[0];
+
+ expect(diff.added).toBeUndefined();
+ expect(diff.modified).toBe(modified);
+ expect(diff.removed).toBeTruthy();
+ expect(diff.lineNumber).toBe(lineNumber);
+ },
+ );
+ });
+
+ it('includes line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.lineNumber).toBe(1);
+ });
+
+ it('includes end line number of change', () => {
+ const diff = computeDiff('123', '')[0];
+
+ expect(diff.endLineNumber).toBe(1);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/lib/editor_options_spec.js b/spec/frontend/ide/lib/editor_options_spec.js
index d149a883166..b07a583b7c8 100644
--- a/spec/javascripts/ide/lib/editor_options_spec.js
+++ b/spec/frontend/ide/lib/editor_options_spec.js
@@ -2,7 +2,7 @@ import editorOptions from '~/ide/lib/editor_options';
describe('Multi-file editor library editor options', () => {
it('returns an array', () => {
- expect(editorOptions).toEqual(jasmine.any(Array));
+ expect(editorOptions).toEqual(expect.any(Array));
});
it('contains readOnly option', () => {
diff --git a/spec/frontend/ide/lib/files_spec.js b/spec/frontend/ide/lib/files_spec.js
new file mode 100644
index 00000000000..aa1fa0373db
--- /dev/null
+++ b/spec/frontend/ide/lib/files_spec.js
@@ -0,0 +1,78 @@
+import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
+import { decorateFiles, splitParent, escapeFileUrl } from '~/ide/lib/files';
+import { decorateData } from '~/ide/stores/utils';
+
+const TEST_BRANCH_ID = 'lorem-ipsum';
+const TEST_PROJECT_ID = 10;
+
+const createEntries = paths => {
+ const createEntry = (acc, { path, type, children }) => {
+ // Sometimes we need to end the url with a '/'
+ const createUrl = base => (type === 'tree' ? `${base}/` : base);
+
+ const { name, parent } = splitParent(path);
+ const parentEntry = acc[parent];
+
+ acc[path] = {
+ ...decorateData({
+ projectId: TEST_PROJECT_ID,
+ branchId: TEST_BRANCH_ID,
+ id: path,
+ name,
+ path,
+ url: createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}/-/${escapeFileUrl(path)}`),
+ type,
+ previewMode: viewerInformationForPath(path),
+ parentPath: parent,
+ parentTreeUrl: parentEntry
+ ? parentEntry.url
+ : createUrl(`/${TEST_PROJECT_ID}/${type}/${TEST_BRANCH_ID}`),
+ }),
+ tree: children.map(childName => expect.objectContaining({ name: childName })),
+ };
+
+ return acc;
+ };
+
+ const entries = paths.reduce(createEntry, {});
+
+ // Wrap entries in expect.objectContaining.
+ // We couldn't do this earlier because we still need to select properties from parent entries.
+ return Object.keys(entries).reduce((acc, key) => {
+ acc[key] = expect.objectContaining(entries[key]);
+
+ return acc;
+ }, {});
+};
+
+describe('IDE lib decorate files', () => {
+ it('creates entries and treeList', () => {
+ const data = ['app/assets/apples/foo.js', 'app/bugs.js', 'app/#weird#file?.txt', 'README.md'];
+ const expectedEntries = createEntries([
+ { path: 'app', type: 'tree', children: ['assets', '#weird#file?.txt', 'bugs.js'] },
+ { path: 'app/assets', type: 'tree', children: ['apples'] },
+ { path: 'app/assets/apples', type: 'tree', children: ['foo.js'] },
+ { path: 'app/assets/apples/foo.js', type: 'blob', children: [] },
+ { path: 'app/bugs.js', type: 'blob', children: [] },
+ { path: 'app/#weird#file?.txt', type: 'blob', children: [] },
+ { path: 'README.md', type: 'blob', children: [] },
+ ]);
+
+ const { entries, treeList } = decorateFiles({
+ data,
+ branchId: TEST_BRANCH_ID,
+ projectId: TEST_PROJECT_ID,
+ });
+
+ // Here we test the keys and then each key/value individually because `expect(entries).toEqual(expectedEntries)`
+ // was taking a very long time for some reason. Probably due to large objects and nested `expect.objectContaining`.
+ const entryKeys = Object.keys(entries);
+
+ expect(entryKeys).toEqual(Object.keys(expectedEntries));
+ entryKeys.forEach(key => {
+ expect(entries[key]).toEqual(expectedEntries[key]);
+ });
+
+ expect(treeList).toEqual([expectedEntries.app, expectedEntries['README.md']]);
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
index 5de7a281d34..246500a2f34 100644
--- a/spec/javascripts/ide/stores/modules/commit/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/mutations_spec.js
@@ -18,7 +18,7 @@ describe('IDE commit module mutations', () => {
describe('UPDATE_COMMIT_ACTION', () => {
it('updates commitAction', () => {
- mutations.UPDATE_COMMIT_ACTION(state, 'testing');
+ mutations.UPDATE_COMMIT_ACTION(state, { commitAction: 'testing' });
expect(state.commitAction).toBe('testing');
});
@@ -39,4 +39,35 @@ describe('IDE commit module mutations', () => {
expect(state.submitCommitLoading).toBeTruthy();
});
});
+
+ describe('TOGGLE_SHOULD_CREATE_MR', () => {
+ it('changes shouldCreateMR to true when initial state is false', () => {
+ state.shouldCreateMR = false;
+ mutations.TOGGLE_SHOULD_CREATE_MR(state);
+
+ expect(state.shouldCreateMR).toBe(true);
+ });
+
+ it('changes shouldCreateMR to false when initial state is true', () => {
+ state.shouldCreateMR = true;
+ mutations.TOGGLE_SHOULD_CREATE_MR(state);
+
+ expect(state.shouldCreateMR).toBe(false);
+ });
+
+ it('sets shouldCreateMR to given value when passed in', () => {
+ state.shouldCreateMR = false;
+ mutations.TOGGLE_SHOULD_CREATE_MR(state, false);
+
+ expect(state.shouldCreateMR).toBe(false);
+ });
+ });
+
+ describe('INTERACT_WITH_NEW_MR', () => {
+ it('sets interactedWithNewMR to true', () => {
+ mutations.INTERACT_WITH_NEW_MR(state);
+
+ expect(state.interactedWithNewMR).toBe(true);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js
index 17cb457881f..17cb457881f 100644
--- a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js
diff --git a/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js
new file mode 100644
index 00000000000..6a1a826093c
--- /dev/null
+++ b/spec/frontend/ide/stores/modules/file_templates/mutations_spec.js
@@ -0,0 +1,88 @@
+import createState from '~/ide/stores/modules/file_templates/state';
+import * as types from '~/ide/stores/modules/file_templates/mutation_types';
+import mutations from '~/ide/stores/modules/file_templates/mutations';
+
+const mockFileTemplates = [['MIT'], ['CC']];
+const mockTemplateType = 'test';
+
+describe('IDE file templates mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe(`${types.REQUEST_TEMPLATE_TYPES}`, () => {
+ it('sets loading to true', () => {
+ state.isLoading = false;
+
+ mutations[types.REQUEST_TEMPLATE_TYPES](state);
+
+ expect(state.isLoading).toBe(true);
+ });
+
+ it('sets templates to an empty array', () => {
+ state.templates = mockFileTemplates;
+
+ mutations[types.REQUEST_TEMPLATE_TYPES](state);
+
+ expect(state.templates).toEqual([]);
+ });
+ });
+
+ describe(`${types.RECEIVE_TEMPLATE_TYPES_ERROR}`, () => {
+ it('sets isLoading', () => {
+ state.isLoading = true;
+
+ mutations[types.RECEIVE_TEMPLATE_TYPES_ERROR](state);
+
+ expect(state.isLoading).toBe(false);
+ });
+ });
+
+ describe(`${types.RECEIVE_TEMPLATE_TYPES_SUCCESS}`, () => {
+ it('sets isLoading to false', () => {
+ state.isLoading = true;
+
+ mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, mockFileTemplates);
+
+ expect(state.isLoading).toBe(false);
+ });
+
+ it('sets templates to payload', () => {
+ state.templates = ['test'];
+
+ mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, mockFileTemplates);
+
+ expect(state.templates).toEqual(mockFileTemplates);
+ });
+ });
+
+ describe(`${types.SET_SELECTED_TEMPLATE_TYPE}`, () => {
+ it('sets templates type to selected type', () => {
+ state.selectedTemplateType = '';
+
+ mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, mockTemplateType);
+
+ expect(state.selectedTemplateType).toBe(mockTemplateType);
+ });
+
+ it('sets templates to empty array', () => {
+ state.templates = mockFileTemplates;
+
+ mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, mockTemplateType);
+
+ expect(state.templates).toEqual([]);
+ });
+ });
+
+ describe(`${types.SET_UPDATE_SUCCESS}`, () => {
+ it('sets updateSuccess', () => {
+ state.updateSuccess = false;
+
+ mutations[types.SET_UPDATE_SUCCESS](state, true);
+
+ expect(state.updateSuccess).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/modules/pane/getters_spec.js b/spec/frontend/ide/stores/modules/pane/getters_spec.js
index 8a213323de0..8a213323de0 100644
--- a/spec/javascripts/ide/stores/modules/pane/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/pane/getters_spec.js
diff --git a/spec/javascripts/ide/stores/modules/pane/mutations_spec.js b/spec/frontend/ide/stores/modules/pane/mutations_spec.js
index b5fcd35912e..b5fcd35912e 100644
--- a/spec/javascripts/ide/stores/modules/pane/mutations_spec.js
+++ b/spec/frontend/ide/stores/modules/pane/mutations_spec.js
diff --git a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js b/spec/frontend/ide/stores/modules/pipelines/getters_spec.js
index 4514896b5ea..4514896b5ea 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/getters_spec.js
diff --git a/spec/frontend/ide/stores/mutations/branch_spec.js b/spec/frontend/ide/stores/mutations/branch_spec.js
new file mode 100644
index 00000000000..0900b25d5d3
--- /dev/null
+++ b/spec/frontend/ide/stores/mutations/branch_spec.js
@@ -0,0 +1,75 @@
+import mutations from '~/ide/stores/mutations/branch';
+import state from '~/ide/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ });
+
+ describe('SET_CURRENT_BRANCH', () => {
+ it('sets currentBranch', () => {
+ mutations.SET_CURRENT_BRANCH(localState, 'master');
+
+ expect(localState.currentBranchId).toBe('master');
+ });
+ });
+
+ describe('SET_BRANCH_COMMIT', () => {
+ it('sets the last commit on current project', () => {
+ localState.projects = {
+ Example: {
+ branches: {
+ master: {},
+ },
+ },
+ };
+
+ mutations.SET_BRANCH_COMMIT(localState, {
+ projectId: 'Example',
+ branchId: 'master',
+ commit: {
+ title: 'Example commit',
+ },
+ });
+
+ expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
+ });
+ });
+
+ describe('SET_BRANCH_WORKING_REFERENCE', () => {
+ beforeEach(() => {
+ localState.projects = {
+ Foo: {
+ branches: {
+ bar: {},
+ },
+ },
+ };
+ });
+
+ it('sets workingReference for existing branch', () => {
+ mutations.SET_BRANCH_WORKING_REFERENCE(localState, {
+ projectId: 'Foo',
+ branchId: 'bar',
+ reference: 'foo-bar-ref',
+ });
+
+ expect(localState.projects.Foo.branches.bar.workingReference).toBe('foo-bar-ref');
+ });
+
+ it('does not fail on non-existent just yet branch', () => {
+ expect(localState.projects.Foo.branches.unknown).toBeUndefined();
+
+ mutations.SET_BRANCH_WORKING_REFERENCE(localState, {
+ projectId: 'Foo',
+ branchId: 'unknown',
+ reference: 'fun-fun-ref',
+ });
+
+ expect(localState.projects.Foo.branches.unknown).not.toBeUndefined();
+ expect(localState.projects.Foo.branches.unknown.workingReference).toBe('fun-fun-ref');
+ });
+ });
+});
diff --git a/spec/javascripts/ide/stores/mutations/merge_request_spec.js b/spec/frontend/ide/stores/mutations/merge_request_spec.js
index e30ca22022f..afbe6770c0d 100644
--- a/spec/javascripts/ide/stores/mutations/merge_request_spec.js
+++ b/spec/frontend/ide/stores/mutations/merge_request_spec.js
@@ -32,6 +32,24 @@ describe('IDE store merge request mutations', () => {
expect(newMr.title).toBe('mr');
expect(newMr.active).toBeTruthy();
});
+
+ it('keeps original data', () => {
+ const versions = ['change'];
+ const mergeRequest = localState.projects.abcproject.mergeRequests[1];
+
+ mergeRequest.versions = versions;
+
+ mutations.SET_MERGE_REQUEST(localState, {
+ projectPath: 'abcproject',
+ mergeRequestId: 1,
+ mergeRequest: {
+ title: ['change'],
+ },
+ });
+
+ expect(mergeRequest.title).toBe('mr');
+ expect(mergeRequest.versions).toEqual(versions);
+ });
});
describe('SET_MERGE_REQUEST_CHANGES', () => {
diff --git a/spec/frontend/ide/stores/mutations/project_spec.js b/spec/frontend/ide/stores/mutations/project_spec.js
new file mode 100644
index 00000000000..b3ce39c33d2
--- /dev/null
+++ b/spec/frontend/ide/stores/mutations/project_spec.js
@@ -0,0 +1,23 @@
+import mutations from '~/ide/stores/mutations/project';
+import state from '~/ide/stores/state';
+
+describe('Multi-file store branch mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = state();
+ localState.projects = { abcproject: { empty_repo: true } };
+ });
+
+ describe('TOGGLE_EMPTY_STATE', () => {
+ it('sets empty_repo for project to passed value', () => {
+ mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: false });
+
+ expect(localState.projects.abcproject.empty_repo).toBe(false);
+
+ mutations.TOGGLE_EMPTY_STATE(localState, { projectPath: 'abcproject', value: true });
+
+ expect(localState.projects.abcproject.empty_repo).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/image_diff/view_types_spec.js b/spec/frontend/image_diff/view_types_spec.js
index e9639f46497..e9639f46497 100644
--- a/spec/javascripts/image_diff/view_types_spec.js
+++ b/spec/frontend/image_diff/view_types_spec.js
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
new file mode 100644
index 00000000000..17a998d0174
--- /dev/null
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -0,0 +1,185 @@
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
+import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
+import STATUS_MAP from '~/import_projects/constants';
+
+describe('ImportProjectsTable', () => {
+ let vm;
+ const providerTitle = 'THE PROVIDER';
+ const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
+ const importedProject = {
+ id: 1,
+ fullPath: 'fullPath',
+ importStatus: 'started',
+ providerLink: 'providerLink',
+ importSource: 'importSource',
+ };
+
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchJobs: jest.fn(),
+ fetchRepos: jest.fn(actions.requestRepos),
+ fetchImport: jest.fn(actions.requestImport),
+ });
+
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+
+ const component = mount(importProjectsTable, {
+ localVue,
+ store,
+ propsData: {
+ providerTitle,
+ },
+ sync: false,
+ });
+
+ return component.vm;
+ }
+
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders a loading icon whilst repos are loading', () =>
+ vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
+ }));
+
+ it('renders a table with imported projects and provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).not.toBeNull();
+ expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
+ `From ${providerTitle}`,
+ );
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+ });
+ });
+
+ it('renders an empty state if there are no imported projects or provider repos', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [],
+ namespaces: [],
+ });
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
+ expect(vm.$el.querySelector('.table')).toBeNull();
+ expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
+ });
+ });
+
+ it('shows loading spinner when bulk import button is clicked', () => {
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$el.querySelector('.js-import-all').click();
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-import-all .js-loading-button-icon')).not.toBeNull();
+ });
+ });
+
+ it('imports provider repos if bulk import button is clicked', () => {
+ mountComponent();
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [],
+ providerRepos: [providerRepo],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
+
+ vm.$store.dispatch('receiveImportSuccess', { importedProject, repoId: providerRepo.id });
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
+ });
+ });
+
+ it('polls to update the status of imported projects', () => {
+ const updatedProjects = [
+ {
+ id: importedProject.id,
+ importStatus: 'finished',
+ },
+ ];
+
+ vm.$store.dispatch('receiveReposSuccess', {
+ importedProjects: [importedProject],
+ providerRepos: [],
+ namespaces: [{ path: 'path' }],
+ });
+
+ return vm
+ .$nextTick()
+ .then(() => {
+ const statusObject = STATUS_MAP[importedProject.importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+
+ vm.$store.dispatch('receiveJobsSuccess', updatedProjects);
+ })
+ .then(() => vm.$nextTick())
+ .then(() => {
+ const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
+
+ expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
+ expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
+ statusObject.text,
+ );
+
+ expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
index 8af3b5954a9..f95acc1edd7 100644
--- a/spec/javascripts/import_projects/components/imported_project_table_row_spec.js
+++ b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
@@ -1,5 +1,6 @@
-import Vue from 'vue';
-import store from '~/import_projects/store';
+import Vuex from 'vuex';
+import createStore from '~/import_projects/store';
+import { createLocalVue, mount } from '@vue/test-utils';
import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import STATUS_MAP from '~/import_projects/constants';
@@ -13,26 +14,33 @@ describe('ImportedProjectTableRow', () => {
importSource: 'importSource',
};
- function createComponent() {
- const ImportedProjectTableRow = Vue.extend(importedProjectTableRow);
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
- return new ImportedProjectTableRow({
- store,
+ const component = mount(importedProjectTableRow, {
+ localVue,
+ store: createStore(),
propsData: {
project: {
...project,
},
},
- }).$mount();
+ sync: false,
+ });
+
+ return component.vm;
}
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
afterEach(() => {
vm.$destroy();
});
it('renders an imported project table row', () => {
- vm = createComponent();
-
const providerLink = vm.$el.querySelector('.js-provider-link');
const statusObject = STATUS_MAP[project.importStatus];
diff --git a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index 69377f8d685..02c786d8d0b 100644
--- a/spec/javascripts/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -1,13 +1,15 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import store from '~/import_projects/store';
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import { state, actions, getters, mutations } from '~/import_projects/store';
import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
describe('ProviderRepoTableRow', () => {
let vm;
+ const fetchImport = jest.fn((context, data) => actions.requestImport(context, data));
+ const importPath = '/import-path';
+ const defaultTargetNamespace = 'user';
+ const ciCdOnly = true;
const repo = {
id: 10,
sanitizedName: 'sanitizedName',
@@ -15,26 +17,49 @@ describe('ProviderRepoTableRow', () => {
providerLink: 'providerLink',
};
- function createComponent() {
- const ProviderRepoTableRow = Vue.extend(providerRepoTableRow);
+ function initStore() {
+ const stubbedActions = Object.assign({}, actions, {
+ fetchImport,
+ });
- return new ProviderRepoTableRow({
+ const store = new Vuex.Store({
+ state: state(),
+ actions: stubbedActions,
+ mutations,
+ getters,
+ });
+
+ return store;
+ }
+
+ function mountComponent() {
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const store = initStore();
+ store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
+
+ const component = mount(providerRepoTableRow, {
+ localVue,
store,
propsData: {
- repo: {
- ...repo,
- },
+ repo,
},
- }).$mount();
+ sync: false,
+ });
+
+ return component.vm;
}
+ beforeEach(() => {
+ vm = mountComponent();
+ });
+
afterEach(() => {
vm.$destroy();
});
it('renders a provider repo table row', () => {
- vm = createComponent();
-
const providerLink = vm.$el.querySelector('.js-provider-link');
const statusObject = STATUS_MAP[STATUSES.NONE];
@@ -50,8 +75,6 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a select2 namespace select', () => {
- vm = createComponent();
-
const dropdownTrigger = vm.$el.querySelector('.js-namespace-select');
expect(dropdownTrigger).not.toBeNull();
@@ -62,30 +85,20 @@ describe('ProviderRepoTableRow', () => {
expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
});
- it('imports repo when clicking import button', done => {
- const importPath = '/import-path';
- const defaultTargetNamespace = 'user';
- const ciCdOnly = true;
- const mock = new MockAdapter(axios);
-
- store.dispatch('setInitialData', { importPath, defaultTargetNamespace, ciCdOnly });
- mock.onPost(importPath).replyOnce(200);
- spyOn(store, 'dispatch').and.returnValue(new Promise(() => {}));
-
- vm = createComponent();
-
+ it('imports repo when clicking import button', () => {
vm.$el.querySelector('.js-import-button').click();
- setTimeoutPromise()
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchImport', {
- repo,
- newName: repo.sanitizedName,
- targetNamespace: defaultTargetNamespace,
- });
- })
- .then(() => mock.restore())
- .then(done)
- .catch(done.fail);
+ return vm.$nextTick().then(() => {
+ const { calls } = fetchImport.mock;
+
+ // Not using .toBeCalledWith because it expects
+ // an unmatchable and undefined 3rd argument.
+ expect(calls.length).toBe(1);
+ expect(calls[0][1]).toEqual({
+ repo,
+ newName: repo.sanitizedName,
+ targetNamespace: defaultTargetNamespace,
+ });
+ });
});
});
diff --git a/spec/javascripts/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index 77850ee3283..6a7b90788dd 100644
--- a/spec/javascripts/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -27,8 +27,8 @@ import {
stopJobsPolling,
} from '~/import_projects/store/actions';
import state from '~/import_projects/store/state';
-import testAction from 'spec/helpers/vuex_action_helper';
-import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'helpers/test_constants';
describe('import_projects store actions', () => {
let localState;
diff --git a/spec/javascripts/import_projects/store/getters_spec.js b/spec/frontend/import_projects/store/getters_spec.js
index e5e4a95f473..e5e4a95f473 100644
--- a/spec/javascripts/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_projects/store/getters_spec.js
diff --git a/spec/javascripts/import_projects/store/mutations_spec.js b/spec/frontend/import_projects/store/mutations_spec.js
index 8db8e9819ba..505545f7aa5 100644
--- a/spec/javascripts/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_projects/store/mutations_spec.js
@@ -2,7 +2,7 @@ import * as types from '~/import_projects/store/mutation_types';
import mutations from '~/import_projects/store/mutations';
describe('import_projects store mutations', () => {
- describe(types.RECEIVE_IMPORT_SUCCESS, () => {
+ describe(`${types.RECEIVE_IMPORT_SUCCESS}`, () => {
it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => {
const repoId = 1;
const state = {
@@ -20,7 +20,7 @@ describe('import_projects store mutations', () => {
});
});
- describe(types.RECEIVE_JOBS_SUCCESS, () => {
+ describe(`${types.RECEIVE_JOBS_SUCCESS}`, () => {
it('updates importStatus of existing importedProjects', () => {
const repoId = 1;
const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] };
diff --git a/spec/javascripts/issuable_suggestions/components/app_spec.js b/spec/frontend/issuable_suggestions/components/app_spec.js
index 7bb8e26b81a..7bb8e26b81a 100644
--- a/spec/javascripts/issuable_suggestions/components/app_spec.js
+++ b/spec/frontend/issuable_suggestions/components/app_spec.js
diff --git a/spec/javascripts/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index 7bd1fe678f4..7bd1fe678f4 100644
--- a/spec/javascripts/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
diff --git a/spec/javascripts/issuable_suggestions/mock_data.js b/spec/frontend/issuable_suggestions/mock_data.js
index 4f0f9ef8d62..4f0f9ef8d62 100644
--- a/spec/javascripts/issuable_suggestions/mock_data.js
+++ b/spec/frontend/issuable_suggestions/mock_data.js
diff --git a/spec/javascripts/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js
index a2df79bdda0..a2df79bdda0 100644
--- a/spec/javascripts/jobs/components/empty_state_spec.js
+++ b/spec/frontend/jobs/components/empty_state_spec.js
diff --git a/spec/javascripts/jobs/components/erased_block_spec.js b/spec/frontend/jobs/components/erased_block_spec.js
index 8e0433d3fb7..8e0433d3fb7 100644
--- a/spec/javascripts/jobs/components/erased_block_spec.js
+++ b/spec/frontend/jobs/components/erased_block_spec.js
diff --git a/spec/javascripts/jobs/components/sidebar_detail_row_spec.js b/spec/frontend/jobs/components/sidebar_detail_row_spec.js
index 42d11266dad..42d11266dad 100644
--- a/spec/javascripts/jobs/components/sidebar_detail_row_spec.js
+++ b/spec/frontend/jobs/components/sidebar_detail_row_spec.js
diff --git a/spec/javascripts/jobs/components/stuck_block_spec.js b/spec/frontend/jobs/components/stuck_block_spec.js
index c320793b2be..c320793b2be 100644
--- a/spec/javascripts/jobs/components/stuck_block_spec.js
+++ b/spec/frontend/jobs/components/stuck_block_spec.js
diff --git a/spec/javascripts/jobs/store/getters_spec.js b/spec/frontend/jobs/store/getters_spec.js
index 7931b2af79f..379114c3737 100644
--- a/spec/javascripts/jobs/store/getters_spec.js
+++ b/spec/frontend/jobs/store/getters_spec.js
@@ -151,6 +151,61 @@ describe('Job Store Getters', () => {
});
});
+ describe('shouldRenderSharedRunnerLimitWarning', () => {
+ describe('without runners information', () => {
+ it('returns false', () => {
+ expect(getters.shouldRenderSharedRunnerLimitWarning(localState)).toEqual(false);
+ });
+ });
+
+ describe('with runners information', () => {
+ describe('when used quota is less than limit', () => {
+ it('returns false', () => {
+ localState.job.runners = {
+ quota: {
+ used: 33,
+ limit: 2000,
+ },
+ available: true,
+ online: true,
+ };
+
+ expect(getters.shouldRenderSharedRunnerLimitWarning(localState)).toEqual(false);
+ });
+ });
+
+ describe('when used quota is equal to limit', () => {
+ it('returns true', () => {
+ localState.job.runners = {
+ quota: {
+ used: 2000,
+ limit: 2000,
+ },
+ available: true,
+ online: true,
+ };
+
+ expect(getters.shouldRenderSharedRunnerLimitWarning(localState)).toEqual(true);
+ });
+ });
+
+ describe('when used quota is bigger than limit', () => {
+ it('returns true', () => {
+ localState.job.runners = {
+ quota: {
+ used: 2002,
+ limit: 2000,
+ },
+ available: true,
+ online: true,
+ };
+
+ expect(getters.shouldRenderSharedRunnerLimitWarning(localState)).toEqual(true);
+ });
+ });
+ });
+ });
+
describe('hasRunnersForProject', () => {
describe('with available and offline runners', () => {
it('returns true', () => {
diff --git a/spec/javascripts/jobs/store/mutations_spec.js b/spec/frontend/jobs/store/mutations_spec.js
index d7908efcf13..343301b8716 100644
--- a/spec/javascripts/jobs/store/mutations_spec.js
+++ b/spec/frontend/jobs/store/mutations_spec.js
@@ -150,44 +150,8 @@ describe('Jobs Store Mutations', () => {
});
});
- describe('REQUEST_STAGES', () => {
- it('sets isLoadingStages to true', () => {
- mutations[types.REQUEST_STAGES](stateCopy);
-
- expect(stateCopy.isLoadingStages).toEqual(true);
- });
- });
-
- describe('RECEIVE_STAGES_SUCCESS', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_STAGES_SUCCESS](stateCopy, [{ name: 'build' }]);
- });
-
- it('sets isLoadingStages to false', () => {
- expect(stateCopy.isLoadingStages).toEqual(false);
- });
-
- it('sets stages', () => {
- expect(stateCopy.stages).toEqual([{ name: 'build' }]);
- });
- });
-
- describe('RECEIVE_STAGES_ERROR', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_STAGES_ERROR](stateCopy);
- });
-
- it('sets isLoadingStages to false', () => {
- expect(stateCopy.isLoadingStages).toEqual(false);
- });
-
- it('resets stages', () => {
- expect(stateCopy.stages).toEqual([]);
- });
- });
-
describe('REQUEST_JOBS_FOR_STAGE', () => {
- it('sets isLoadingStages to true', () => {
+ it('sets isLoadingJobs to true', () => {
mutations[types.REQUEST_JOBS_FOR_STAGE](stateCopy, { name: 'deploy' });
expect(stateCopy.isLoadingJobs).toEqual(true);
diff --git a/spec/frontend/labels_select_spec.js b/spec/frontend/labels_select_spec.js
new file mode 100644
index 00000000000..d54e0eab845
--- /dev/null
+++ b/spec/frontend/labels_select_spec.js
@@ -0,0 +1,116 @@
+import $ from 'jquery';
+import LabelsSelect from '~/labels_select';
+
+const mockUrl = '/foo/bar/url';
+
+const mockLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+];
+
+const mockScopedLabels = [
+ {
+ id: 27,
+ title: 'Foo::Bar',
+ description: 'Foobar',
+ color: '#333ABC',
+ text_color: '#FFFFFF',
+ },
+];
+
+describe('LabelsSelect', () => {
+ describe('getLabelTemplate', () => {
+ describe('when normal label is present', () => {
+ const label = mockLabels[0];
+ let $labelEl;
+
+ beforeEach(() => {
+ $labelEl = $(
+ LabelsSelect.getLabelTemplate({
+ labels: mockLabels,
+ issueUpdateURL: mockUrl,
+ enableScopedLabels: true,
+ scopedLabelsDocumentationLink: 'docs-link',
+ }),
+ );
+ });
+
+ it('generated label item template has correct label URL', () => {
+ expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
+ });
+
+ it('generated label item template has correct label title', () => {
+ expect($labelEl.find('span.label').text()).toBe(label.title);
+ });
+
+ it('generated label item template has label description as title attribute', () => {
+ expect($labelEl.find('span.label').attr('title')).toBe(label.description);
+ });
+
+ it('generated label item template has correct label styles', () => {
+ expect($labelEl.find('span.label').attr('style')).toBe(
+ `background-color: ${label.color}; color: ${label.text_color};`,
+ );
+ });
+
+ it('generated label item has a badge class', () => {
+ expect($labelEl.find('span').hasClass('badge')).toEqual(true);
+ });
+
+ it('generated label item template does not have scoped-label class', () => {
+ expect($labelEl.find('.scoped-label')).toHaveLength(0);
+ });
+ });
+
+ describe('when scoped label is present', () => {
+ const label = mockScopedLabels[0];
+ let $labelEl;
+
+ beforeEach(() => {
+ $labelEl = $(
+ LabelsSelect.getLabelTemplate({
+ labels: mockScopedLabels,
+ issueUpdateURL: mockUrl,
+ enableScopedLabels: true,
+ scopedLabelsDocumentationLink: 'docs-link',
+ }),
+ );
+ });
+
+ it('generated label item template has correct label URL', () => {
+ expect($labelEl.find('a').attr('href')).toBe('/foo/bar?label_name[]=Foo%3A%3ABar');
+ });
+
+ it('generated label item template has correct label title', () => {
+ expect($labelEl.find('span.label').text()).toBe(label.title);
+ });
+
+ it('generated label item template has html flag as true', () => {
+ expect($labelEl.find('span.label').attr('data-html')).toBe('true');
+ });
+
+ it('generated label item template has question icon', () => {
+ expect($labelEl.find('i.fa-question-circle')).toHaveLength(1);
+ });
+
+ it('generated label item template has scoped-label class', () => {
+ expect($labelEl.find('.scoped-label')).toHaveLength(1);
+ });
+
+ it('generated label item template has correct label styles', () => {
+ expect($labelEl.find('span.label').attr('style')).toBe(
+ `background-color: ${label.color}; color: ${label.text_color};`,
+ );
+ });
+
+ it('generated label item has a badge class', () => {
+ expect($labelEl.find('span').hasClass('badge')).toEqual(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/autosave_spec.js b/spec/frontend/lib/utils/autosave_spec.js
new file mode 100644
index 00000000000..12e97f6cdec
--- /dev/null
+++ b/spec/frontend/lib/utils/autosave_spec.js
@@ -0,0 +1,64 @@
+import { clearDraft, getDraft, updateDraft } from '~/lib/utils/autosave';
+
+describe('autosave utils', () => {
+ const autosaveKey = 'dummy-autosave-key';
+ const text = 'some dummy text';
+
+ describe('clearDraft', () => {
+ beforeEach(() => {
+ localStorage.setItem(`autosave/${autosaveKey}`, text);
+ });
+
+ afterEach(() => {
+ localStorage.removeItem(`autosave/${autosaveKey}`);
+ });
+
+ it('removes the draft from localStorage', () => {
+ clearDraft(autosaveKey);
+
+ expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(null);
+ });
+ });
+
+ describe('getDraft', () => {
+ beforeEach(() => {
+ localStorage.setItem(`autosave/${autosaveKey}`, text);
+ });
+
+ afterEach(() => {
+ localStorage.removeItem(`autosave/${autosaveKey}`);
+ });
+
+ it('returns the draft from localStorage', () => {
+ const result = getDraft(autosaveKey);
+
+ expect(result).toBe(text);
+ });
+
+ it('returns null if no entry exists in localStorage', () => {
+ localStorage.removeItem(`autosave/${autosaveKey}`);
+
+ const result = getDraft(autosaveKey);
+
+ expect(result).toBe(null);
+ });
+ });
+
+ describe('updateDraft', () => {
+ beforeEach(() => {
+ localStorage.setItem(`autosave/${autosaveKey}`, text);
+ });
+
+ afterEach(() => {
+ localStorage.removeItem(`autosave/${autosaveKey}`);
+ });
+
+ it('removes the draft from localStorage', () => {
+ const newText = 'new text';
+
+ updateDraft(autosaveKey, newText);
+
+ expect(localStorage.getItem(`autosave/${autosaveKey}`)).toBe(newText);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/cache_spec.js b/spec/frontend/lib/utils/cache_spec.js
index 2fe02a7592c..2fe02a7592c 100644
--- a/spec/javascripts/lib/utils/cache_spec.js
+++ b/spec/frontend/lib/utils/cache_spec.js
diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 5327ec9d2a0..9f49e68cfe8 100644
--- a/spec/javascripts/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -65,6 +65,26 @@ describe('Date time utils', () => {
});
});
+ describe('formatDate', () => {
+ it('should format date properly', () => {
+ const formattedDate = datetimeUtility.formatDate(new Date('07/23/2016'));
+
+ expect(formattedDate).toBe('Jul 23, 2016 12:00am GMT+0000');
+ });
+
+ it('should format ISO date properly', () => {
+ const formattedDate = datetimeUtility.formatDate('2016-07-23T00:00:00.559Z');
+
+ expect(formattedDate).toBe('Jul 23, 2016 12:00am GMT+0000');
+ });
+
+ it('should throw an error if date is invalid', () => {
+ expect(() => {
+ datetimeUtility.formatDate('2016-07-23 00:00:00 UTC');
+ }).toThrow(new Error('Invalid date'));
+ });
+ });
+
describe('get day difference', () => {
it('should return 7', () => {
const firstDay = new Date('07/01/2016');
@@ -380,7 +400,7 @@ describe('prettyTime methods', () => {
describe('calculateRemainingMilliseconds', () => {
beforeEach(() => {
- spyOn(Date, 'now').and.callFake(() => new Date('2063-04-04T00:42:00Z').getTime());
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
});
it('calculates the remaining time for a given end date', () => {
diff --git a/spec/javascripts/lib/utils/grammar_spec.js b/spec/frontend/lib/utils/grammar_spec.js
index 377b2ffb48c..377b2ffb48c 100644
--- a/spec/javascripts/lib/utils/grammar_spec.js
+++ b/spec/frontend/lib/utils/grammar_spec.js
diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/frontend/lib/utils/image_utility_spec.js
index a7eff419fba..a7eff419fba 100644
--- a/spec/javascripts/lib/utils/image_utility_spec.js
+++ b/spec/frontend/lib/utils/image_utility_spec.js
diff --git a/spec/javascripts/lib/utils/number_utility_spec.js b/spec/frontend/lib/utils/number_utility_spec.js
index 94c6214c86a..77d7478d317 100644
--- a/spec/javascripts/lib/utils/number_utility_spec.js
+++ b/spec/frontend/lib/utils/number_utility_spec.js
@@ -4,6 +4,8 @@ import {
bytesToMiB,
bytesToGiB,
numberToHumanSize,
+ sum,
+ isOdd,
} from '~/lib/utils/number_utils';
describe('Number Utils', () => {
@@ -87,4 +89,24 @@ describe('Number Utils', () => {
expect(numberToHumanSize(10737418240)).toEqual('10.00 GiB');
});
});
+
+ describe('sum', () => {
+ it('should add up two values', () => {
+ expect(sum(1, 2)).toEqual(3);
+ });
+
+ it('should add up all the values in an array when passed to a reducer', () => {
+ expect([1, 2, 3, 4, 5].reduce(sum)).toEqual(15);
+ });
+ });
+
+ describe('isOdd', () => {
+ it('should return 0 with a even number', () => {
+ expect(isOdd(2)).toEqual(0);
+ });
+
+ it('should return 1 with a odd number', () => {
+ expect(isOdd(1)).toEqual(1);
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/frontend/lib/utils/text_utility_spec.js
index 0a266b19ea5..9e920d59093 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/frontend/lib/utils/text_utility_spec.js
@@ -23,14 +23,6 @@ describe('text_utility', () => {
});
});
- describe('capitalizeFirstCharacter', () => {
- it('returns string with first letter capitalized', () => {
- expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
- expect(textUtils.highCountTrim(105)).toBe('99+');
- expect(textUtils.highCountTrim(100)).toBe('99+');
- });
- });
-
describe('humanize', () => {
it('should remove underscores and uppercase the first letter', () => {
expect(textUtils.humanize('foo_bar')).toEqual('Foo bar');
@@ -57,9 +49,9 @@ describe('text_utility', () => {
});
});
- describe('slugify', () => {
- it('should remove accents and convert to lower case', () => {
- expect(textUtils.slugify('João')).toEqual('joão');
+ describe('capitalizeFirstCharacter', () => {
+ it('returns string with first letter capitalized', () => {
+ expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab');
});
});
@@ -151,4 +143,37 @@ describe('text_utility', () => {
);
});
});
+
+ describe('slugifyWithUnderscore', () => {
+ it('should replaces whitespaces with underscore and convert to lower case', () => {
+ expect(textUtils.slugifyWithUnderscore('My Input String')).toEqual('my_input_string');
+ });
+ });
+
+ describe('truncateNamespace', () => {
+ it(`should return the root namespace if the namespace only includes one level`, () => {
+ expect(textUtils.truncateNamespace('a / b')).toBe('a');
+ });
+
+ it(`should return the first 2 namespaces if the namespace includes exactly 2 levels`, () => {
+ expect(textUtils.truncateNamespace('a / b / c')).toBe('a / b');
+ });
+
+ it(`should return the first and last namespaces, separated by "...", if the namespace includes more than 2 levels`, () => {
+ expect(textUtils.truncateNamespace('a / b / c / d')).toBe('a / ... / c');
+ expect(textUtils.truncateNamespace('a / b / c / d / e / f / g / h / i')).toBe('a / ... / h');
+ });
+
+ it(`should return an empty string for invalid inputs`, () => {
+ [undefined, null, 4, {}, true, new Date()].forEach(input => {
+ expect(textUtils.truncateNamespace(input)).toBe('');
+ });
+ });
+
+ it(`should not alter strings that aren't formatted as namespaces`, () => {
+ ['', ' ', '\t', 'a', 'a \\ b'].forEach(input => {
+ expect(textUtils.truncateNamespace(input)).toBe(input);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index 381c7b2d0a6..eca240c9c18 100644
--- a/spec/javascripts/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -107,4 +107,88 @@ describe('URL utility', () => {
expect(url).toBe('/home/feature#install');
});
});
+
+ describe('getBaseURL', () => {
+ beforeEach(() => {
+ global.window = Object.create(window);
+ Object.defineProperty(window, 'location', {
+ value: {
+ host: 'gitlab.com',
+ protocol: 'https:',
+ },
+ });
+ });
+
+ it('returns correct base URL', () => {
+ expect(urlUtils.getBaseURL()).toBe('https://gitlab.com');
+ });
+ });
+
+ describe('isAbsoluteOrRootRelative', () => {
+ const validUrls = ['https://gitlab.com/', 'http://gitlab.com/', '/users/sign_in'];
+
+ const invalidUrls = [' https://gitlab.com/', './file/path', 'notanurl', '<a></a>'];
+
+ it.each(validUrls)(`returns true for %s`, url => {
+ expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(true);
+ });
+
+ it.each(invalidUrls)(`returns false for %s`, url => {
+ expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(false);
+ });
+ });
+
+ describe('isSafeUrl', () => {
+ const absoluteUrls = [
+ 'http://example.org',
+ 'http://example.org:8080',
+ 'https://example.org',
+ 'https://example.org:8080',
+ 'https://192.168.1.1',
+ ];
+
+ const rootRelativeUrls = ['/relative/link'];
+
+ const relativeUrls = ['./relative/link', '../relative/link'];
+
+ const urlsWithoutHost = ['http://', 'https://', 'https:https:https:'];
+
+ /* eslint-disable no-script-url */
+ const nonHttpUrls = [
+ 'javascript:',
+ 'javascript:alert("XSS")',
+ 'jav\tascript:alert("XSS");',
+ ' &#14; javascript:alert("XSS");',
+ 'ftp://192.168.1.1',
+ 'file:///',
+ 'file:///etc/hosts',
+ ];
+ /* eslint-enable no-script-url */
+
+ // javascript:alert('XSS')
+ const encodedJavaScriptUrls = [
+ '&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041',
+ '&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;',
+ '&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29',
+ '\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029',
+ ];
+
+ const safeUrls = [...absoluteUrls, ...rootRelativeUrls];
+ const unsafeUrls = [
+ ...relativeUrls,
+ ...urlsWithoutHost,
+ ...nonHttpUrls,
+ ...encodedJavaScriptUrls,
+ ];
+
+ describe('with URL constructor support', () => {
+ it.each(safeUrls)('returns true for %s', url => {
+ expect(urlUtils.isSafeURL(url)).toBe(true);
+ });
+
+ it.each(unsafeUrls)('returns false for %s', url => {
+ expect(urlUtils.isSafeURL(url)).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/locale/ensure_single_line_spec.js b/spec/frontend/locale/ensure_single_line_spec.js
index 20b04cab9c8..20b04cab9c8 100644
--- a/spec/javascripts/locale/ensure_single_line_spec.js
+++ b/spec/frontend/locale/ensure_single_line_spec.js
diff --git a/spec/javascripts/locale/sprintf_spec.js b/spec/frontend/locale/sprintf_spec.js
index 52e903b819f..52e903b819f 100644
--- a/spec/javascripts/locale/sprintf_spec.js
+++ b/spec/frontend/locale/sprintf_spec.js
diff --git a/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
new file mode 100644
index 00000000000..a2a7d0ee91e
--- /dev/null
+++ b/spec/frontend/mr_popover/__snapshots__/mr_popover_spec.js.snap
@@ -0,0 +1,95 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MR Popover loaded state matches the snapshot 1`] = `
+<glpopover-stub
+ boundary="viewport"
+ cssclasses=""
+ placement="top"
+ show=""
+ target=""
+>
+ <div
+ class="mr-popover"
+ >
+ <div
+ class="d-flex align-items-center justify-content-between"
+ >
+ <div
+ class="d-inline-flex align-items-center"
+ >
+ <div
+ class="issuable-status-box status-box status-box-open"
+ >
+
+ Open
+
+ </div>
+
+ <span
+ class="text-secondary"
+ >
+ Opened
+ <time>
+ just now
+ </time>
+ </span>
+ </div>
+
+ <ciicon-stub
+ cssclasses=""
+ size="16"
+ status="[object Object]"
+ />
+ </div>
+
+ <h5
+ class="my-2"
+ >
+ MR Title
+ </h5>
+
+ <div
+ class="text-secondary"
+ >
+
+ foo/bar!1
+
+ </div>
+ </div>
+</glpopover-stub>
+`;
+
+exports[`MR Popover shows skeleton-loader while apollo is loading 1`] = `
+<glpopover-stub
+ boundary="viewport"
+ cssclasses=""
+ placement="top"
+ show=""
+ target=""
+>
+ <div
+ class="mr-popover"
+ >
+ <div>
+ <glskeletonloading-stub
+ class="animation-container-small mt-1"
+ lines="1"
+ />
+ </div>
+
+ <h5
+ class="my-2"
+ >
+ MR Title
+ </h5>
+
+ <div
+ class="text-secondary"
+ >
+
+ foo/bar!1
+
+ </div>
+ </div>
+</glpopover-stub>
+`;
diff --git a/spec/frontend/mr_popover/index_spec.js b/spec/frontend/mr_popover/index_spec.js
new file mode 100644
index 00000000000..b9db2342687
--- /dev/null
+++ b/spec/frontend/mr_popover/index_spec.js
@@ -0,0 +1,46 @@
+import * as createDefaultClient from '~/lib/graphql';
+import { setHTMLFixture } from '../helpers/fixtures';
+import initMRPopovers from '~/mr_popover/index';
+
+createDefaultClient.default = jest.fn();
+
+describe('initMRPopovers', () => {
+ let mr1;
+ let mr2;
+ let mr3;
+
+ beforeEach(() => {
+ setHTMLFixture(`
+ <div id="one" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project">
+ MR1
+ </div>
+ <div id="two" class="gfm-merge_request" data-mr-title="title" data-iid="1" data-project-path="group/project">
+ MR2
+ </div>
+ <div id="three" class="gfm-merge_request">
+ MR3
+ </div>
+ `);
+
+ mr1 = document.querySelector('#one');
+ mr2 = document.querySelector('#two');
+ mr3 = document.querySelector('#three');
+
+ mr1.addEventListener = jest.fn();
+ mr2.addEventListener = jest.fn();
+ mr3.addEventListener = jest.fn();
+ });
+
+ it('does not add the same event listener twice', () => {
+ initMRPopovers([mr1, mr1, mr2]);
+
+ expect(mr1.addEventListener).toHaveBeenCalledTimes(1);
+ expect(mr2.addEventListener).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not add listener if it does not have the necessary data attributes', () => {
+ initMRPopovers([mr1, mr2, mr3]);
+
+ expect(mr3.addEventListener).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/mr_popover/mr_popover_spec.js b/spec/frontend/mr_popover/mr_popover_spec.js
new file mode 100644
index 00000000000..79ed4163010
--- /dev/null
+++ b/spec/frontend/mr_popover/mr_popover_spec.js
@@ -0,0 +1,61 @@
+import MRPopover from '~/mr_popover/components/mr_popover';
+import { shallowMount } from '@vue/test-utils';
+
+describe('MR Popover', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallowMount(MRPopover, {
+ propsData: {
+ target: document.createElement('a'),
+ projectPath: 'foo/bar',
+ mergeRequestIID: '1',
+ mergeRequestTitle: 'MR Title',
+ },
+ mocks: {
+ $apollo: {
+ loading: false,
+ },
+ },
+ });
+ });
+
+ it('shows skeleton-loader while apollo is loading', () => {
+ wrapper.vm.$apollo.loading = true;
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('loaded state', () => {
+ it('matches the snapshot', () => {
+ wrapper.setData({
+ mergeRequest: {
+ state: 'opened',
+ createdAt: new Date(),
+ headPipeline: {
+ detailedStatus: {
+ group: 'success',
+ status: 'status_success',
+ },
+ },
+ },
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('does not show CI Icon if there is no pipeline data', () => {
+ wrapper.setData({
+ mergeRequest: {
+ state: 'opened',
+ headPipeline: null,
+ stateHumanName: 'Open',
+ title: 'Merge Request Title',
+ createdAt: new Date(),
+ },
+ });
+
+ expect(wrapper.contains('ciicon-stub')).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/lib/highlight_spec.js b/spec/frontend/notebook/lib/highlight_spec.js
index d71c5718858..d71c5718858 100644
--- a/spec/javascripts/notebook/lib/highlight_spec.js
+++ b/spec/frontend/notebook/lib/highlight_spec.js
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
new file mode 100644
index 00000000000..0a52c81571e
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -0,0 +1,104 @@
+import createStore from '~/notes/stores';
+import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
+import { discussionMock } from '../../../javascripts/notes/mock_data';
+import DiscussionActions from '~/notes/components/discussion_actions.vue';
+import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
+import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
+import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
+import JumpToNextDiscussionButton from '~/notes/components/discussion_jump_to_next_button.vue';
+
+describe('DiscussionActions', () => {
+ let wrapper;
+ const createComponentFactory = (shallow = true) => props => {
+ const localVue = createLocalVue();
+ const store = createStore();
+ const mountFn = shallow ? shallowMount : mount;
+
+ wrapper = mountFn(DiscussionActions, {
+ localVue,
+ store,
+ propsData: {
+ discussion: discussionMock,
+ isResolving: false,
+ resolveButtonTitle: 'Resolve discussion',
+ resolveWithIssuePath: '/some/issue/path',
+ shouldShowJumpToNextDiscussion: true,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ const createComponent = createComponentFactory();
+
+ it('renders reply placeholder, resolve discussion button, resolve with issue button and jump to next discussion button', () => {
+ createComponent();
+ expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(true);
+ expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(true);
+ expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(true);
+ });
+
+ it('only renders reply placholder if disccusion is not resolvable', () => {
+ const discussion = { ...discussionMock };
+ discussion.resolvable = false;
+ createComponent({ discussion });
+
+ expect(wrapper.find(ReplyPlaceholder).exists()).toBe(true);
+ expect(wrapper.find(ResolveDiscussionButton).exists()).toBe(false);
+ expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
+ expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
+ });
+
+ it('does not render resolve with issue button if resolveWithIssuePath is falsy', () => {
+ createComponent({ resolveWithIssuePath: '' });
+
+ expect(wrapper.find(ResolveWithIssueButton).exists()).toBe(false);
+ });
+
+ it('does not render jump to next discussion button if shouldShowJumpToNextDiscussion is false', () => {
+ createComponent({ shouldShowJumpToNextDiscussion: false });
+
+ expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
+ });
+ });
+
+ describe('events handling', () => {
+ const createComponent = createComponentFactory(false);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('emits showReplyForm event when clicking on reply placeholder', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+ wrapper
+ .find(ReplyPlaceholder)
+ .find('button')
+ .trigger('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('showReplyForm');
+ });
+
+ it('emits resolve event when clicking on resolve button', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+ wrapper
+ .find(ResolveDiscussionButton)
+ .find('button')
+ .trigger('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('resolve');
+ });
+
+ it('emits jumpToNextDiscussion event when clicking on jump to next discussion button', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+ wrapper
+ .find(JumpToNextDiscussionButton)
+ .find('button')
+ .trigger('click');
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('jumpToNextDiscussion');
+ });
+ });
+});
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
new file mode 100644
index 00000000000..c3204b3aaa0
--- /dev/null
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -0,0 +1,139 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import '~/behaviors/markdown/render_gfm';
+import { SYSTEM_NOTE } from '~/notes/constants';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import NoteableNote from '~/notes/components/noteable_note.vue';
+import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
+import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
+import SystemNote from '~/vue_shared/components/notes/system_note.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import createStore from '~/notes/stores';
+import {
+ noteableDataMock,
+ discussionMock,
+ notesDataMock,
+} from '../../../javascripts/notes/mock_data';
+
+const localVue = createLocalVue();
+
+describe('DiscussionNotes', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ const store = createStore();
+ store.dispatch('setNoteableData', noteableDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+
+ wrapper = shallowMount(DiscussionNotes, {
+ localVue,
+ store,
+ propsData: {
+ discussion: discussionMock,
+ isExpanded: false,
+ shouldGroupReplies: false,
+ ...props,
+ },
+ scopedSlots: {
+ footer: '<p slot-scope="{ showReplies }">showReplies:{{showReplies}}</p>',
+ },
+ slots: {
+ 'avatar-badge': '<span class="avatar-badge-slot-content" />',
+ },
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('rendering', () => {
+ it('renders an element for each note in the discussion', () => {
+ createComponent();
+ const notesCount = discussionMock.notes.length;
+ const els = wrapper.findAll(TimelineEntryItem);
+ expect(els.length).toBe(notesCount);
+ });
+
+ it('renders one element if replies groupping is enabled', () => {
+ createComponent({ shouldGroupReplies: true });
+ const els = wrapper.findAll(TimelineEntryItem);
+ expect(els.length).toBe(1);
+ });
+
+ it('uses proper component to render each note type', () => {
+ const discussion = { ...discussionMock };
+ const notesData = [
+ // PlaceholderSystemNote
+ {
+ id: 1,
+ isPlaceholderNote: true,
+ placeholderType: SYSTEM_NOTE,
+ notes: [{ body: 'PlaceholderSystemNote' }],
+ },
+ // PlaceholderNote
+ {
+ id: 2,
+ isPlaceholderNote: true,
+ notes: [{ body: 'PlaceholderNote' }],
+ },
+ // SystemNote
+ {
+ id: 3,
+ system: true,
+ note: 'SystemNote',
+ },
+ // NoteableNote
+ discussion.notes[0],
+ ];
+ discussion.notes = notesData;
+ createComponent({ discussion });
+ const notes = wrapper.findAll('.notes > li');
+
+ expect(notes.at(0).is(PlaceholderSystemNote)).toBe(true);
+ expect(notes.at(1).is(PlaceholderNote)).toBe(true);
+ expect(notes.at(2).is(SystemNote)).toBe(true);
+ expect(notes.at(3).is(NoteableNote)).toBe(true);
+ });
+
+ it('renders footer scoped slot with showReplies === true when expanded', () => {
+ createComponent({ isExpanded: true });
+ expect(wrapper.text()).toMatch('showReplies:true');
+ });
+
+ it('renders footer scoped slot with showReplies === false when collapsed', () => {
+ createComponent({ isExpanded: false });
+ expect(wrapper.text()).toMatch('showReplies:false');
+ });
+
+ it('passes down avatar-badge slot content', () => {
+ createComponent();
+ expect(wrapper.find('.avatar-badge-slot-content').exists()).toBe(true);
+ });
+ });
+
+ describe('componentData', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should return first note object for placeholder note', () => {
+ const data = {
+ isPlaceholderNote: true,
+ notes: [{ body: 'hello world!' }],
+ };
+ const note = wrapper.vm.componentData(data);
+
+ expect(note).toEqual(data.notes[0]);
+ });
+
+ it('should return given note for nonplaceholder notes', () => {
+ const data = {
+ notes: [{ id: 12 }],
+ };
+ const note = wrapper.vm.componentData(data);
+
+ expect(note).toEqual(data);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/discussion_reply_placeholder_spec.js b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
index 07a366cf339..07a366cf339 100644
--- a/spec/javascripts/notes/components/discussion_reply_placeholder_spec.js
+++ b/spec/frontend/notes/components/discussion_reply_placeholder_spec.js
diff --git a/spec/javascripts/notes/components/discussion_resolve_button_spec.js b/spec/frontend/notes/components/discussion_resolve_button_spec.js
index 5024f40ec5d..5024f40ec5d 100644
--- a/spec/javascripts/notes/components/discussion_resolve_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_button_spec.js
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/frontend/notes/components/note_app_spec.js
index 82f58dafc78..ff833d2c899 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/frontend/notes/components/note_app_spec.js
@@ -1,18 +1,47 @@
-import $ from 'jquery';
-import _ from 'underscore';
+import $ from 'helpers/jquery';
import Vue from 'vue';
import { mount, createLocalVue } from '@vue/test-utils';
import NotesApp from '~/notes/components/notes_app.vue';
import service from '~/notes/services/notes_service';
import createStore from '~/notes/stores';
import '~/behaviors/markdown/render_gfm';
-import * as mockData from '../mock_data';
+import { setTestTimeout } from 'helpers/timeout';
+// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-ce/issues/62491)
+import * as mockData from '../../../javascripts/notes/mock_data';
+
+const originalInterceptors = [...Vue.http.interceptors];
+
+const emptyResponseInterceptor = (request, next) => {
+ next(
+ request.respondWith(JSON.stringify([]), {
+ status: 200,
+ }),
+ );
+};
+
+setTestTimeout(1000);
describe('note_app', () => {
let mountComponent;
let wrapper;
let store;
+ /**
+ * waits for fetchNotes() to complete
+ */
+ const waitForDiscussionsRequest = () =>
+ new Promise(resolve => {
+ const { vm } = wrapper.find(NotesApp);
+ const unwatch = vm.$watch('isFetching', isFetching => {
+ if (isFetching) {
+ return;
+ }
+
+ unwatch();
+ resolve();
+ });
+ });
+
beforeEach(() => {
$('body').attr('data-page', 'projects:merge_requests:show');
@@ -33,6 +62,7 @@ describe('note_app', () => {
template: '<div class="js-vue-notes-event"><notes-app v-bind="$attrs" /></div>',
},
{
+ attachToDocument: true,
propsData,
store,
localVue,
@@ -44,24 +74,14 @@ describe('note_app', () => {
afterEach(() => {
wrapper.destroy();
+ Vue.http.interceptors = [...originalInterceptors];
});
describe('set data', () => {
- const responseInterceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify([]), {
- status: 200,
- }),
- );
- };
-
beforeEach(() => {
- Vue.http.interceptors.push(responseInterceptor);
+ Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent();
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, responseInterceptor);
+ return waitForDiscussionsRequest();
});
it('should set notes data', () => {
@@ -87,29 +107,23 @@ describe('note_app', () => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
- });
-
- it('should render list of notes', done => {
+ it('should render list of notes', () => {
const note =
mockData.INDIVIDUAL_NOTE_RESPONSE_MAP.GET[
'/gitlab-org/gitlab-ce/issues/26/discussions.json'
][0].notes[0];
- setTimeout(() => {
- expect(
- wrapper
- .find('.main-notes-list .note-header-author-name')
- .text()
- .trim(),
- ).toEqual(note.author.name);
+ expect(
+ wrapper
+ .find('.main-notes-list .note-header-author-name')
+ .text()
+ .trim(),
+ ).toEqual(note.author.name);
- expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html);
- done();
- }, 0);
+ expect(wrapper.find('.main-notes-list .note-text').html()).toContain(note.note_html);
});
it('should render form', () => {
@@ -120,30 +134,42 @@ describe('note_app', () => {
});
it('should not render form when commenting is disabled', () => {
+ wrapper.destroy();
+
store.state.commentsDisabled = true;
wrapper = mountComponent();
+ return waitForDiscussionsRequest().then(() => {
+ expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
+ });
+ });
+
+ it('should render discussion filter note `commentsDisabled` is true', () => {
+ wrapper.destroy();
- expect(wrapper.find('.js-main-target-form').exists()).toBe(false);
+ store.state.commentsDisabled = true;
+ wrapper = mountComponent();
+ return waitForDiscussionsRequest().then(() => {
+ expect(wrapper.find('.js-discussion-filter-note').exists()).toBe(true);
+ });
});
it('should render form comment button as disabled', () => {
expect(wrapper.find('.js-note-new-discussion').attributes('disabled')).toEqual('disabled');
});
- it('updates discussions badge', done => {
- setTimeout(() => {
- expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
-
- done();
- });
+ it('updates discussions badge', () => {
+ expect(document.querySelector('.js-discussions-count').textContent).toEqual('2');
});
});
describe('while fetching data', () => {
beforeEach(() => {
+ Vue.http.interceptors.push(emptyResponseInterceptor);
wrapper = mountComponent();
});
+ afterEach(() => waitForDiscussionsRequest());
+
it('renders skeleton notes', () => {
expect(wrapper.find('.animation-container').exists()).toBe(true);
});
@@ -158,78 +184,55 @@ describe('note_app', () => {
describe('update note', () => {
describe('individual note', () => {
- beforeEach(done => {
+ beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
- spyOn(service, 'updateNote').and.callThrough();
+ jest.spyOn(service, 'updateNote');
wrapper = mountComponent();
- setTimeout(() => {
+ return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click');
- Vue.nextTick(done);
- }, 0);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors,
- mockData.individualNoteInterceptor,
- );
+ });
});
it('renders edit form', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
});
- it('calls the service to update the note', done => {
+ it('calls the service to update the note', () => {
wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click');
expect(service.updateNote).toHaveBeenCalled();
- // Wait for the requests to finish before destroying
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
});
});
describe('discussion note', () => {
- beforeEach(done => {
+ beforeEach(() => {
Vue.http.interceptors.push(mockData.discussionNoteInterceptor);
- spyOn(service, 'updateNote').and.callThrough();
+ jest.spyOn(service, 'updateNote');
wrapper = mountComponent();
-
- setTimeout(() => {
+ return waitForDiscussionsRequest().then(() => {
wrapper.find('.js-note-edit').trigger('click');
- Vue.nextTick(done);
- }, 0);
- });
-
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors,
- mockData.discussionNoteInterceptor,
- );
+ });
});
it('renders edit form', () => {
expect(wrapper.find('.js-vue-issue-note-form').exists()).toBe(true);
});
- it('updates the note and resets the edit form', done => {
+ it('updates the note and resets the edit form', () => {
wrapper.find('.js-vue-issue-note-form').value = 'this is a note';
wrapper.find('.js-vue-issue-save').trigger('click');
expect(service.updateNote).toHaveBeenCalled();
- // Wait for the requests to finish before destroying
- Vue.nextTick()
- .then(done)
- .catch(done.fail);
});
});
});
describe('new note form', () => {
beforeEach(() => {
+ Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
it('should render markdown docs url', () => {
@@ -259,43 +262,37 @@ describe('note_app', () => {
beforeEach(() => {
Vue.http.interceptors.push(mockData.individualNoteInterceptor);
wrapper = mountComponent();
+ return waitForDiscussionsRequest();
});
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, mockData.individualNoteInterceptor);
- });
+ it('should render markdown docs url', () => {
+ wrapper.find('.js-note-edit').trigger('click');
+ const { markdownDocsPath } = mockData.notesDataMock;
- it('should render markdown docs url', done => {
- setTimeout(() => {
- wrapper.find('.js-note-edit').trigger('click');
- const { markdownDocsPath } = mockData.notesDataMock;
-
- Vue.nextTick(() => {
- expect(
- wrapper
- .find(`.edit-note a[href="${markdownDocsPath}"]`)
- .text()
- .trim(),
- ).toEqual('Markdown is supported');
- done();
- });
- }, 0);
+ return Vue.nextTick().then(() => {
+ expect(
+ wrapper
+ .find(`.edit-note a[href="${markdownDocsPath}"]`)
+ .text()
+ .trim(),
+ ).toEqual('Markdown is supported');
+ });
});
- it('should not render quick actions docs url', done => {
- setTimeout(() => {
- wrapper.find('.js-note-edit').trigger('click');
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- Vue.nextTick(() => {
- expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
- done();
- });
- }, 0);
+ it('should not render quick actions docs url', () => {
+ wrapper.find('.js-note-edit').trigger('click');
+ const { quickActionsDocsPath } = mockData.notesDataMock;
+ expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).exists()).toBe(false);
});
});
describe('emoji awards', () => {
+ beforeEach(() => {
+ Vue.http.interceptors.push(emptyResponseInterceptor);
+ wrapper = mountComponent();
+ return waitForDiscussionsRequest();
+ });
+
it('dispatches toggleAward after toggleAward event', () => {
const toggleAwardEvent = new CustomEvent('toggleAward', {
detail: {
@@ -303,17 +300,18 @@ describe('note_app', () => {
noteId: 1,
},
});
- const toggleAwardAction = jasmine.createSpy('toggleAward');
+ const toggleAwardAction = jest.fn().mockName('toggleAward');
wrapper.vm.$store.hotUpdate({
actions: {
toggleAward: toggleAwardAction,
+ stopPolling() {},
},
});
wrapper.vm.$parent.$el.dispatchEvent(toggleAwardEvent);
expect(toggleAwardAction).toHaveBeenCalledTimes(1);
- const [, payload] = toggleAwardAction.calls.argsFor(0);
+ const [, payload] = toggleAwardAction.mock.calls[0];
expect(payload).toEqual({
awardName: 'test',
diff --git a/spec/javascripts/notes/components/note_attachment_spec.js b/spec/frontend/notes/components/note_attachment_spec.js
index b14a518b622..b14a518b622 100644
--- a/spec/javascripts/notes/components/note_attachment_spec.js
+++ b/spec/frontend/notes/components/note_attachment_spec.js
diff --git a/spec/javascripts/notes/components/note_edited_text_spec.js b/spec/frontend/notes/components/note_edited_text_spec.js
index e4c8d954d50..e4c8d954d50 100644
--- a/spec/javascripts/notes/components/note_edited_text_spec.js
+++ b/spec/frontend/notes/components/note_edited_text_spec.js
diff --git a/spec/javascripts/notes_spec.js b/spec/frontend/notes/old_notes_spec.js
index 7c869d4c326..b57041cf4d1 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/frontend/notes/old_notes_spec.js
@@ -1,84 +1,89 @@
-/* eslint-disable no-unused-expressions, no-var, object-shorthand */
+/* eslint-disable import/no-commonjs, no-new */
+
import $ from 'jquery';
import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import 'autosize';
-import '~/gl_form';
-import '~/lib/utils/text_utility';
+import * as urlUtility from '~/lib/utils/url_utility';
import '~/behaviors/markdown/render_gfm';
-import Notes from '~/notes';
-import timeoutPromise from './helpers/set_timeout_promise_helper';
-
-window.gon || (window.gon = {});
+import { createSpyObj } from 'helpers/jest_helpers';
+import { setTestTimeoutOnce } from 'helpers/timeout';
+import { TEST_HOST } from 'helpers/test_constants';
+
+// These must be imported synchronously because they pull dependencies
+// from the DOM.
+window.jQuery = $;
+require('autosize');
+require('~/commons');
+require('~/notes');
+
+const { Notes } = window;
+const FLASH_TYPE_ALERT = 'alert';
+const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
+const fixture = 'snippets/show.html';
+let mockAxios;
+
+window.project_uploads_path = `${TEST_HOST}/uploads`;
+window.gon = window.gon || {};
window.gl = window.gl || {};
gl.utils = gl.utils || {};
+gl.utils.disableButtonIfEmptyField = () => {};
-const htmlEscape = comment => {
- const escapedString = comment.replace(/["&'<>]/g, a => {
- const escapedToken = {
- '&': '&amp;',
- '<': '&lt;',
- '>': '&gt;',
- '"': '&quot;',
- "'": '&#x27;',
- '`': '&#x60;',
- }[a];
-
- return escapedToken;
- });
+describe('Old Notes (~/notes.js)', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ loadFixtures(fixture);
+
+ // Re-declare this here so that test_setup.js#beforeEach() doesn't
+ // overwrite it.
+ mockAxios = new MockAdapter(axios);
- return escapedString;
-};
+ $.ajax = () => {
+ throw new Error('$.ajax should not be called through!');
+ };
-describe('Notes', function() {
- const FLASH_TYPE_ALERT = 'alert';
- const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
- var fixture = 'snippets/show.html.raw';
- preloadFixtures(fixture);
+ // These jQuery+DOM tests are super flaky so increase the timeout to avoid
+ // random failures.
+ // It seems that running tests in parallel increases failure rate.
+ jest.setTimeout(4000);
+ setTestTimeoutOnce(4000);
+ });
- beforeEach(function() {
- loadFixtures(fixture);
- gl.utils.disableButtonIfEmptyField = _.noop;
- window.project_uploads_path = 'http://test.host/uploads';
- $('body').attr('data-page', 'projects:merge_requets:show');
+ afterEach(done => {
+ // The Notes component sets a polling interval. Clear it after every run.
+ // Make sure to use jest.runOnlyPendingTimers() instead of runAllTimers().
+ jest.clearAllTimers();
+
+ setImmediate(() => {
+ // Wait for any requests to resolve, otherwise we get failures about
+ // unmocked requests.
+ mockAxios.restore();
+ done();
+ });
});
- afterEach(() => {
- // Undo what we did to the shared <body>
- $('body').removeAttr('data-page');
+ it('loads the Notes class into the DOM', () => {
+ expect(Notes).toBeDefined();
+ expect(Notes.name).toBe('Notes');
});
describe('addBinding', () => {
it('calls postComment when comment button is clicked', () => {
- spyOn(Notes.prototype, 'postComment');
- this.notes = new Notes('', []);
+ jest.spyOn(Notes.prototype, 'postComment');
+ new window.Notes('', []);
$('.js-comment-button').click();
-
expect(Notes.prototype.postComment).toHaveBeenCalled();
});
});
- describe('task lists', function() {
- let mock;
-
- beforeEach(function() {
- spyOn(axios, 'patch').and.callFake(() => new Promise(() => {}));
- mock = new MockAdapter(axios);
- mock.onAny().reply(200, {});
-
- $('.js-comment-button').on('click', function(e) {
- e.preventDefault();
- });
- this.notes = new Notes('', []);
- });
-
- afterEach(() => {
- mock.restore();
+ describe('task lists', () => {
+ beforeEach(() => {
+ mockAxios.onAny().reply(200, {});
+ new Notes('', []);
});
- it('modifies the Markdown field', function() {
+ it('modifies the Markdown field', () => {
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
$('input[type=checkbox]')
@@ -88,7 +93,9 @@ describe('Notes', function() {
expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
});
- it('submits an ajax request on tasklist:changed', function(done) {
+ it('submits an ajax request on tasklist:changed', () => {
+ jest.spyOn(axios, 'patch');
+
const lineNumber = 8;
const lineSource = '- [ ] item 8';
const index = 3;
@@ -99,76 +106,74 @@ describe('Notes', function() {
detail: { lineNumber, lineSource, index, checked },
});
- setTimeout(() => {
- expect(axios.patch).toHaveBeenCalledWith(undefined, {
- note: {
- note: '',
- lock_version: undefined,
- update_task: { index, checked, line_number: lineNumber, line_source: lineSource },
- },
- });
-
- done();
+ expect(axios.patch).toHaveBeenCalledWith(undefined, {
+ note: {
+ note: '',
+ lock_version: undefined,
+ update_task: { index, checked, line_number: lineNumber, line_source: lineSource },
+ },
});
});
});
- describe('comments', function() {
- var textarea = '.js-note-text';
-
- beforeEach(function() {
- this.notes = new Notes('', []);
+ describe('comments', () => {
+ let notes;
+ let autosizeSpy;
+ let textarea;
- this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
- spyOn(this.notes, 'renderNote').and.stub();
+ beforeEach(() => {
+ notes = new Notes('', []);
- $(textarea).data('autosave', {
- reset: function() {},
+ textarea = $('.js-note-text');
+ textarea.data('autosave', {
+ reset: () => {},
});
+ autosizeSpy = jest.fn();
+ $(textarea).on('autosize:update', autosizeSpy);
+
+ jest.spyOn(notes, 'renderNote');
$('.js-comment-button').on('click', e => {
const $form = $(this);
e.preventDefault();
- this.notes.addNote($form);
- this.notes.reenableTargetFormSubmitButton(e);
- this.notes.resetMainTargetForm(e);
+ notes.addNote($form, {});
+ notes.reenableTargetFormSubmitButton(e);
+ notes.resetMainTargetForm(e);
});
});
- it('autosizes after comment submission', function() {
- $(textarea).text('This is an example comment note');
-
- expect(this.autoSizeSpy).not.toHaveBeenTriggered();
-
+ it('autosizes after comment submission', () => {
+ textarea.text('This is an example comment note');
+ expect(autosizeSpy).not.toHaveBeenCalled();
$('.js-comment-button').click();
-
- expect(this.autoSizeSpy).toHaveBeenTriggered();
+ expect(autosizeSpy).toHaveBeenCalled();
});
- it('should not place escaped text in the comment box in case of error', function() {
+ it('should not place escaped text in the comment box in case of error', () => {
const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ jest.spyOn($, 'ajax').mockReturnValueOnce(deferred);
$(textarea).text('A comment with `markup`.');
deferred.reject();
$('.js-comment-button').click();
- expect($(textarea).val()).toEqual('A comment with `markup`.');
+ expect($(textarea).val()).toBe('A comment with `markup`.');
+
+ $.ajax.mockRestore();
+ expect($.ajax.mock).toBeUndefined();
});
});
describe('updateNote', () => {
- let sampleComment;
+ let notes;
let noteEntity;
- let $form;
let $notesContainer;
- let mock;
beforeEach(() => {
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
- sampleComment = 'foo';
+ const sampleComment = 'foo';
noteEntity = {
id: 1234,
html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
@@ -177,35 +182,27 @@ describe('Notes', function() {
note: sampleComment,
valid: true,
};
- $form = $('form.js-main-target-form');
+
$notesContainer = $('ul.main-notes-list');
+ const $form = $('form.js-main-target-form');
$form.find('textarea.js-note-text').val(sampleComment);
- mock = new MockAdapter(axios);
- mock.onPost(NOTES_POST_PATH).reply(200, noteEntity);
- });
-
- afterEach(() => {
- mock.restore();
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, noteEntity);
});
- it('updates note and resets edit form', done => {
- spyOn(this.notes, 'revertNoteEditForm');
- spyOn(this.notes, 'setupNewNote');
+ it('updates note and resets edit form', () => {
+ jest.spyOn(notes, 'revertNoteEditForm');
+ jest.spyOn(notes, 'setupNewNote');
$('.js-comment-button').click();
- setTimeout(() => {
- const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
- const updatedNote = Object.assign({}, noteEntity);
- updatedNote.note = 'bar';
- this.notes.updateNote(updatedNote, $targetNote);
+ const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
+ const updatedNote = Object.assign({}, noteEntity);
+ updatedNote.note = 'bar';
+ notes.updateNote(updatedNote, $targetNote);
- expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
- expect(this.notes.setupNewNote).toHaveBeenCalled();
-
- done();
- });
+ expect(notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
+ expect(notes.setupNewNote).toHaveBeenCalled();
});
});
@@ -215,32 +212,44 @@ describe('Notes', function() {
beforeEach(() => {
$note = $(`<div id="${hash}"></div>`);
- spyOn($note, 'filter').and.callThrough();
- spyOn($note, 'toggleClass').and.callThrough();
+ jest.spyOn($note, 'filter');
+ jest.spyOn($note, 'toggleClass');
+ });
+
+ afterEach(() => {
+ expect(typeof urlUtility.getLocationHash.mock).toBe('object');
+ urlUtility.getLocationHash.mockRestore();
+ expect(urlUtility.getLocationHash.mock).toBeUndefined();
+ expect(urlUtility.getLocationHash()).toBeNull();
});
+ // urlUtility is a dependency of the notes module. Its getLocatinHash() method should be called internally.
+
it('sets target when hash matches', () => {
- spyOnDependency(Notes, 'getLocationHash').and.returnValue(hash);
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(hash);
Notes.updateNoteTargetSelector($note);
+ expect(urlUtility.getLocationHash).toHaveBeenCalled();
expect($note.filter).toHaveBeenCalledWith(`#${hash}`);
expect($note.toggleClass).toHaveBeenCalledWith('target', true);
});
it('unsets target when hash does not match', () => {
- spyOnDependency(Notes, 'getLocationHash').and.returnValue('note_doesnotexist');
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce('note_doesnotexist');
Notes.updateNoteTargetSelector($note);
+ expect(urlUtility.getLocationHash).toHaveBeenCalled();
expect($note.toggleClass).toHaveBeenCalledWith('target', false);
});
it('unsets target when there is not a hash fragment anymore', () => {
- spyOnDependency(Notes, 'getLocationHash').and.returnValue(null);
+ jest.spyOn(urlUtility, 'getLocationHash').mockReturnValueOnce(null);
Notes.updateNoteTargetSelector($note);
+ expect(urlUtility.getLocationHash).toHaveBeenCalled();
expect($note.toggleClass).toHaveBeenCalledWith('target', false);
});
});
@@ -257,28 +266,28 @@ describe('Notes', function() {
note: 'heya',
html: '<div>heya</div>',
};
- $notesList = jasmine.createSpyObj('$notesList', ['find', 'append']);
+ $notesList = createSpyObj('$notesList', ['find', 'append']);
- notes = jasmine.createSpyObj('notes', [
+ notes = createSpyObj('notes', [
'setupNewNote',
'refresh',
'collapseLongCommitList',
'updateNotesCount',
'putConflictEditWarningInPlace',
]);
- notes.taskList = jasmine.createSpyObj('tasklist', ['init']);
+ notes.taskList = createSpyObj('tasklist', ['init']);
notes.note_ids = [];
notes.updatedNotesTrackingMap = {};
- spyOn(Notes, 'isNewNote').and.callThrough();
- spyOn(Notes, 'isUpdatedNote').and.callThrough();
- spyOn(Notes, 'animateAppendNote').and.callThrough();
- spyOn(Notes, 'animateUpdateNote').and.callThrough();
+ jest.spyOn(Notes, 'isNewNote');
+ jest.spyOn(Notes, 'isUpdatedNote');
+ jest.spyOn(Notes, 'animateAppendNote');
+ jest.spyOn(Notes, 'animateUpdateNote');
});
describe('when adding note', () => {
it('should call .animateAppendNote', () => {
- Notes.isNewNote.and.returnValue(true);
+ Notes.isNewNote.mockReturnValueOnce(true);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
expect(Notes.animateAppendNote).toHaveBeenCalledWith(note.html, $notesList);
@@ -287,12 +296,12 @@ describe('Notes', function() {
describe('when note was edited', () => {
it('should call .animateUpdateNote', () => {
- Notes.isNewNote.and.returnValue(false);
- Notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.mockReturnValueOnce(false);
+ Notes.isUpdatedNote.mockReturnValueOnce(true);
const $note = $('<div>');
- $notesList.find.and.returnValue($note);
+ $notesList.find.mockReturnValueOnce($note);
const $newNote = $(note.html);
- Notes.animateUpdateNote.and.returnValue($newNote);
+ Notes.animateUpdateNote.mockReturnValueOnce($newNote);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
@@ -302,26 +311,26 @@ describe('Notes', function() {
describe('while editing', () => {
it('should update textarea if nothing has been touched', () => {
- Notes.isNewNote.and.returnValue(false);
- Notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.mockReturnValueOnce(false);
+ Notes.isUpdatedNote.mockReturnValueOnce(true);
const $note = $(`<div class="is-editing">
<div class="original-note-content">initial</div>
<textarea class="js-note-text">initial</textarea>
</div>`);
- $notesList.find.and.returnValue($note);
+ $notesList.find.mockReturnValueOnce($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
expect($note.find('.js-note-text').val()).toEqual(note.note);
});
it('should call .putConflictEditWarningInPlace', () => {
- Notes.isNewNote.and.returnValue(false);
- Notes.isUpdatedNote.and.returnValue(true);
+ Notes.isNewNote.mockReturnValueOnce(false);
+ Notes.isUpdatedNote.mockReturnValueOnce(true);
const $note = $(`<div class="is-editing">
<div class="original-note-content">initial</div>
<textarea class="js-note-text">different</textarea>
</div>`);
- $notesList.find.and.returnValue($note);
+ $notesList.find.mockReturnValueOnce($note);
Notes.prototype.renderNote.call(notes, note, null, $notesList);
expect(notes.putConflictEditWarningInPlace).toHaveBeenCalledWith(note, $note);
@@ -386,32 +395,32 @@ describe('Notes', function() {
discussion_resolvable: false,
diff_discussion_html: false,
};
- $form = jasmine.createSpyObj('$form', ['closest', 'find']);
+ $form = createSpyObj('$form', ['closest', 'find']);
$form.length = 1;
- row = jasmine.createSpyObj('row', ['prevAll', 'first', 'find']);
+ row = createSpyObj('row', ['prevAll', 'first', 'find']);
- notes = jasmine.createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
+ notes = createSpyObj('notes', ['isParallelView', 'updateNotesCount']);
notes.note_ids = [];
- spyOn(Notes, 'isNewNote');
- spyOn(Notes, 'animateAppendNote');
- Notes.isNewNote.and.returnValue(true);
- notes.isParallelView.and.returnValue(false);
- row.prevAll.and.returnValue(row);
- row.first.and.returnValue(row);
- row.find.and.returnValue(row);
+ jest.spyOn(Notes, 'isNewNote');
+ jest.spyOn(Notes, 'animateAppendNote').mockImplementation();
+ Notes.isNewNote.mockReturnValue(true);
+ notes.isParallelView.mockReturnValue(false);
+ row.prevAll.mockReturnValue(row);
+ row.first.mockReturnValue(row);
+ row.find.mockReturnValue(row);
});
describe('Discussion root note', () => {
let body;
beforeEach(() => {
- body = jasmine.createSpyObj('body', ['attr']);
+ body = createSpyObj('body', ['attr']);
discussionContainer = { length: 0 };
- $form.closest.and.returnValues(row, $form);
- $form.find.and.returnValues(discussionContainer);
- body.attr.and.returnValue('');
+ $form.closest.mockReturnValueOnce(row).mockReturnValue($form);
+ $form.find.mockReturnValue(discussionContainer);
+ body.attr.mockReturnValue('');
});
it('should call Notes.animateAppendNote', () => {
@@ -432,7 +441,9 @@ describe('Notes', function() {
line.id = note.discussion_line_code;
document.body.appendChild(line);
- $form.closest.and.returnValues($form);
+ // Override mocks for this single test
+ $form.closest.mockReset();
+ $form.closest.mockReturnValue($form);
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
@@ -444,8 +455,8 @@ describe('Notes', function() {
beforeEach(() => {
discussionContainer = { length: 1 };
- $form.closest.and.returnValues(row, $form);
- $form.find.and.returnValues(discussionContainer);
+ $form.closest.mockReturnValueOnce(row).mockReturnValueOnce($form);
+ $form.find.mockReturnValue(discussionContainer);
Notes.prototype.renderDiscussionNote.call(notes, note, $form);
});
@@ -463,7 +474,7 @@ describe('Notes', function() {
beforeEach(() => {
noteHTML = '<div></div>';
- $notesList = jasmine.createSpyObj('$notesList', ['append']);
+ $notesList = createSpyObj('$notesList', ['append']);
$resultantNote = Notes.animateAppendNote(noteHTML, $notesList);
});
@@ -484,7 +495,7 @@ describe('Notes', function() {
beforeEach(() => {
noteHTML = '<div></div>';
- $note = jasmine.createSpyObj('$note', ['replaceWith']);
+ $note = createSpyObj('$note', ['replaceWith']);
$updatedNote = Notes.animateUpdateNote(noteHTML, $note);
});
@@ -515,7 +526,6 @@ describe('Notes', function() {
describe('postComment & updateComment', () => {
const sampleComment = 'foo';
- const updatedComment = 'bar';
const note = {
id: 1234,
html: `<li class="note note-row-1234 timeline-entry" id="note_1234">
@@ -524,22 +534,20 @@ describe('Notes', function() {
note: sampleComment,
valid: true,
};
+ let notes;
let $form;
let $notesContainer;
- let mock;
function mockNotesPost() {
- mock.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
}
function mockNotesPostError() {
- mock.onPost(NOTES_POST_PATH).networkError();
+ mockAxios.onPost(NOTES_POST_PATH).networkError();
}
beforeEach(() => {
- mock = new MockAdapter(axios);
-
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
$form = $('form.js-main-target-form');
@@ -547,10 +555,6 @@ describe('Notes', function() {
$form.find('textarea.js-note-text').val(sampleComment);
});
- afterEach(() => {
- mock.restore();
- });
-
it('should show placeholder note while new comment is being posted', () => {
mockNotesPost();
@@ -564,9 +568,8 @@ describe('Notes', function() {
$('.js-comment-button').click();
- setTimeout(() => {
+ setImmediate(() => {
expect($notesContainer.find('.note.being-posted').length).toEqual(0);
-
done();
});
});
@@ -580,12 +583,12 @@ describe('Notes', function() {
preventDefault() {},
target: $submitButton,
};
- mock.onPost(NOTES_POST_PATH).replyOnce(() => {
+ mockAxios.onPost(NOTES_POST_PATH).replyOnce(() => {
expect($submitButton).toBeDisabled();
return [200, note];
});
- this.notes
+ notes
.postComment(dummyEvent)
.then(() => {
expect($submitButton).not.toBeDisabled();
@@ -600,9 +603,8 @@ describe('Notes', function() {
$('.js-comment-button').click();
- setTimeout(() => {
+ setImmediate(() => {
expect($notesContainer.find(`#note_${note.id}`).length).toBeGreaterThan(0);
-
done();
});
});
@@ -612,48 +614,49 @@ describe('Notes', function() {
$('.js-comment-button').click();
- setTimeout(() => {
+ setImmediate(() => {
expect($form.find('textarea.js-note-text').val()).toEqual('');
-
done();
});
});
it('should show flash error message when new comment failed to be posted', done => {
mockNotesPostError();
+ jest.spyOn(notes, 'addFlash');
$('.js-comment-button').click();
- setTimeout(() => {
- expect(
- $notesContainer
- .parent()
- .find('.flash-container .flash-text')
- .is(':visible'),
- ).toEqual(true);
-
+ setImmediate(() => {
+ expect(notes.addFlash).toHaveBeenCalled();
+ // JSDom doesn't support the :visible selector yet
+ expect(notes.flashContainer.style.display).not.toBe('none');
done();
});
});
+ // This is a bad test carried over from the Karma -> Jest migration.
+ // The corresponding test in the Karma suite tests for
+ // elements and methods that don't actually exist, and gives a false
+ // positive pass.
+ /*
it('should show flash error message when comment failed to be updated', done => {
mockNotesPost();
+ jest.spyOn(notes, 'addFlash').mockName('addFlash');
$('.js-comment-button').click();
- timeoutPromise()
+ deferredPromise()
.then(() => {
const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('.js-note-edit').click();
$noteEl.find('textarea.js-note-text').val(updatedComment);
- mock.restore();
-
mockNotesPostError();
$noteEl.find('.js-comment-save-button').click();
+ notes.updateComment({preventDefault: () => {}});
})
- .then(timeoutPromise)
+ .then(() => deferredPromise())
.then(() => {
const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
@@ -665,12 +668,13 @@ describe('Notes', function() {
.trim(),
).toEqual(sampleComment); // See if comment reverted back to original
- expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
-
+ expect(notes.addFlash).toHaveBeenCalled();
+ expect(notes.flashContainer.style.display).not.toBe('none');
done();
})
.catch(done.fail);
- });
+ }, 5000);
+ */
});
describe('postComment with Slash commands', () => {
@@ -687,13 +691,11 @@ describe('Notes', function() {
};
let $form;
let $notesContainer;
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
- this.notes = new Notes('', []);
+ new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
gl.awardsHandler = {
@@ -710,17 +712,13 @@ describe('Notes', function() {
$form.find('textarea.js-note-text').val(sampleComment);
});
- afterEach(() => {
- mock.restore();
- });
-
it('should remove slash command placeholder when comment with slash commands is done posting', done => {
- spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
+ jest.spyOn(gl.awardsHandler, 'addAwardToEmojiBar');
$('.js-comment-button').click();
expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
- setTimeout(() => {
+ setImmediate(() => {
expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
done();
});
@@ -740,13 +738,11 @@ describe('Notes', function() {
};
let $form;
let $notesContainer;
- let mock;
beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
- this.notes = new Notes('', []);
+ new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
$form = $('form.js-main-target-form');
@@ -754,14 +750,10 @@ describe('Notes', function() {
$form.find('textarea.js-note-text').html(sampleComment);
});
- afterEach(() => {
- mock.restore();
- });
-
it('should not render a script tag', done => {
$('.js-comment-button').click();
- setTimeout(() => {
+ setImmediate(() => {
const $noteEl = $notesContainer.find(`#note_${note.id}`);
$noteEl.find('.js-note-edit').click();
$noteEl.find('textarea.js-note-text').html(updatedComment);
@@ -786,9 +778,10 @@ describe('Notes', function() {
describe('getFormData', () => {
let $form;
let sampleComment;
+ let notes;
beforeEach(() => {
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
$form = $('form');
sampleComment = 'foobar';
@@ -796,7 +789,7 @@ describe('Notes', function() {
it('should return form metadata object from form reference', () => {
$form.find('textarea.js-note-text').val(sampleComment);
- const { formData, formContent, formAction } = this.notes.getFormData($form);
+ const { formData, formContent, formAction } = notes.getFormData($form);
expect(formData.indexOf(sampleComment)).toBeGreaterThan(-1);
expect(formContent).toEqual(sampleComment);
@@ -804,12 +797,12 @@ describe('Notes', function() {
});
it('should return form metadata with sanitized formContent from form reference', () => {
- spyOn(_, 'escape').and.callFake(htmlEscape);
+ jest.spyOn(_, 'escape');
sampleComment = '<script>alert("Boom!");</script>';
$form.find('textarea.js-note-text').val(sampleComment);
- const { formContent } = this.notes.getFormData($form);
+ const { formContent } = notes.getFormData($form);
expect(_.escape).toHaveBeenCalledWith(sampleComment);
expect(formContent).toEqual('&lt;script&gt;alert(&quot;Boom!&quot;);&lt;/script&gt;');
@@ -817,27 +810,29 @@ describe('Notes', function() {
});
describe('hasQuickActions', () => {
+ let notes;
+
beforeEach(() => {
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
});
it('should return true when comment begins with a quick action', () => {
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
- const hasQuickActions = this.notes.hasQuickActions(sampleComment);
+ const hasQuickActions = notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeTruthy();
});
it('should return false when comment does NOT begin with a quick action', () => {
const sampleComment = 'Hey, /unassign Merging this';
- const hasQuickActions = this.notes.hasQuickActions(sampleComment);
+ const hasQuickActions = notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeFalsy();
});
it('should return false when comment does NOT have any quick actions', () => {
const sampleComment = 'Looking good, Awesome!';
- const hasQuickActions = this.notes.hasQuickActions(sampleComment);
+ const hasQuickActions = notes.hasQuickActions(sampleComment);
expect(hasQuickActions).toBeFalsy();
});
@@ -845,25 +840,25 @@ describe('Notes', function() {
describe('stripQuickActions', () => {
it('should strip quick actions from the comment which begins with a quick action', () => {
- this.notes = new Notes();
+ const notes = new Notes();
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign Merging this';
- const stripedComment = this.notes.stripQuickActions(sampleComment);
+ const stripedComment = notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('');
});
it('should strip quick actions from the comment but leaves plain comment if it is present', () => {
- this.notes = new Notes();
+ const notes = new Notes();
const sampleComment = '/wip\n/milestone %1.0\n/merge\n/unassign\nMerging this';
- const stripedComment = this.notes.stripQuickActions(sampleComment);
+ const stripedComment = notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe('Merging this');
});
it('should NOT strip string that has slashes within', () => {
- this.notes = new Notes();
+ const notes = new Notes();
const sampleComment = 'http://127.0.0.1:3000/root/gitlab-shell/issues/1';
- const stripedComment = this.notes.stripQuickActions(sampleComment);
+ const stripedComment = notes.stripQuickActions(sampleComment);
expect(stripedComment).toBe(sampleComment);
});
@@ -875,15 +870,16 @@ describe('Notes', function() {
{ name: 'title', description: 'Change title', params: [{}] },
{ name: 'estimate', description: 'Set time estimate', params: [{}] },
];
+ let notes;
beforeEach(() => {
- this.notes = new Notes();
+ notes = new Notes();
});
it('should return executing quick action description when note has single quick action', () => {
const sampleComment = '/close';
- expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
+ expect(notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
'Applying command to close this issue',
);
});
@@ -891,7 +887,7 @@ describe('Notes', function() {
it('should return generic multiple quick action description when note has multiple quick actions', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
+ expect(notes.getQuickActionDescription(sampleComment, availableQuickActions)).toBe(
'Applying multiple commands',
);
});
@@ -899,7 +895,7 @@ describe('Notes', function() {
it('should return generic quick action description when available quick actions list is not populated', () => {
const sampleComment = '/close\n/title [Duplicate] Issue foobar';
- expect(this.notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
+ expect(notes.getQuickActionDescription(sampleComment)).toBe('Applying command');
});
});
@@ -909,13 +905,14 @@ describe('Notes', function() {
const currentUsername = 'root';
const currentUserFullname = 'Administrator';
const currentUserAvatar = 'avatar_url';
+ let notes;
beforeEach(() => {
- this.notes = new Notes('', []);
+ notes = new Notes('', []);
});
it('should return constructed placeholder element for regular note based on form contents', () => {
- const $tempNote = this.notes.createPlaceholderNote({
+ const $tempNote = notes.createPlaceholderNote({
formContent: sampleComment,
uniqueId,
isDiscussionNote: false,
@@ -929,8 +926,8 @@ describe('Notes', function() {
expect($tempNote.attr('id')).toEqual(uniqueId);
expect($tempNote.hasClass('being-posted')).toBeTruthy();
expect($tempNote.hasClass('fade-in-half')).toBeTruthy();
- $tempNote.find('.timeline-icon > a, .note-header-info > a').each(function() {
- expect($(this).attr('href')).toEqual(`/${currentUsername}`);
+ $tempNote.find('.timeline-icon > a, .note-header-info > a').each((i, el) => {
+ expect(el.getAttribute('href')).toEqual(`/${currentUsername}`);
});
expect($tempNote.find('.timeline-icon .avatar').attr('src')).toEqual(currentUserAvatar);
@@ -958,7 +955,7 @@ describe('Notes', function() {
});
it('should return constructed placeholder element for discussion note based on form contents', () => {
- const $tempNote = this.notes.createPlaceholderNote({
+ const $tempNote = notes.createPlaceholderNote({
formContent: sampleComment,
uniqueId,
isDiscussionNote: true,
@@ -972,7 +969,7 @@ describe('Notes', function() {
it('should return a escaped user name', () => {
const currentUserFullnameXSS = 'Foo <script>alert("XSS")</script>';
- const $tempNote = this.notes.createPlaceholderNote({
+ const $tempNote = notes.createPlaceholderNote({
formContent: sampleComment,
uniqueId,
isDiscussionNote: false,
@@ -994,14 +991,15 @@ describe('Notes', function() {
describe('createPlaceholderSystemNote', () => {
const sampleCommandDescription = 'Applying command to close this issue';
const uniqueId = 'b1234-a4567';
+ let notes;
beforeEach(() => {
- this.notes = new Notes('', []);
- spyOn(_, 'escape').and.callFake(htmlEscape);
+ notes = new Notes('', []);
+ jest.spyOn(_, 'escape');
});
it('should return constructed placeholder element for system note based on form contents', () => {
- const $tempNote = this.notes.createPlaceholderSystemNote({
+ const $tempNote = notes.createPlaceholderSystemNote({
formContent: sampleCommandDescription,
uniqueId,
});
@@ -1020,29 +1018,28 @@ describe('Notes', function() {
});
describe('appendFlash', () => {
- beforeEach(() => {
- this.notes = new Notes();
- });
-
it('shows a flash message', () => {
- this.notes.addFlash('Error message', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
+ const notes = new Notes('', []);
+ notes.addFlash('Error message', FLASH_TYPE_ALERT, notes.parentTimeline.get(0));
- expect($('.flash-alert').is(':visible')).toBeTruthy();
+ const flash = $('.flash-alert')[0];
+ expect(document.contains(flash)).toBe(true);
+ expect(flash.parentNode.style.display).toBe('block');
});
});
describe('clearFlash', () => {
beforeEach(() => {
$(document).off('ajax:success');
- this.notes = new Notes();
});
it('hides visible flash message', () => {
- this.notes.addFlash('Error message 1', FLASH_TYPE_ALERT, this.notes.parentTimeline.get(0));
-
- this.notes.clearFlash();
-
- expect($('.flash-alert').is(':visible')).toBeFalsy();
+ const notes = new Notes('', []);
+ notes.addFlash('Error message 1', FLASH_TYPE_ALERT, notes.parentTimeline.get(0));
+ const flash = $('.flash-alert')[0];
+ notes.clearFlash();
+ expect(flash.parentNode.style.display).toBe('none');
+ expect(notes.flashContainer).toBeNull();
});
});
});
diff --git a/spec/frontend/notes/stores/utils_spec.js b/spec/frontend/notes/stores/utils_spec.js
new file mode 100644
index 00000000000..b31b7491334
--- /dev/null
+++ b/spec/frontend/notes/stores/utils_spec.js
@@ -0,0 +1,17 @@
+import { hasQuickActions } from '~/notes/stores/utils';
+
+describe('hasQuickActions', () => {
+ it.each`
+ input | expected
+ ${'some comment'} | ${false}
+ ${'/quickaction'} | ${true}
+ ${'some comment with\n/quickaction'} | ${true}
+ `('returns $expected for $input', ({ input, expected }) => {
+ expect(hasQuickActions(input)).toBe(expected);
+ });
+
+ it('is stateless', () => {
+ expect(hasQuickActions('some comment')).toBe(hasQuickActions('some comment'));
+ expect(hasQuickActions('/quickaction')).toBe(hasQuickActions('/quickaction'));
+ });
+});
diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js
new file mode 100644
index 00000000000..a881de8fbfe
--- /dev/null
+++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js
@@ -0,0 +1,166 @@
+import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
+import store from '~/operation_settings/store';
+import axios from '~/lib/utils/axios_utils';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import createFlash from '~/flash';
+import { TEST_HOST } from 'helpers/test_constants';
+
+jest.mock('~/lib/utils/axios_utils');
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/flash');
+
+describe('operation settings external dashboard component', () => {
+ let wrapper;
+ const operationsSettingsEndpoint = `${TEST_HOST}/mock/ops/settings/endpoint`;
+ const externalDashboardUrl = `http://mock-external-domain.com/external/dashboard/url`;
+ const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
+ const localVue = createLocalVue();
+ const mountComponent = (shallow = true) => {
+ const config = [
+ ExternalDashboard,
+ {
+ localVue,
+ store: store({
+ operationsSettingsEndpoint,
+ externalDashboardUrl,
+ externalDashboardHelpPagePath,
+ }),
+ },
+ ];
+ wrapper = shallow ? shallowMount(...config) : mount(...config);
+ };
+
+ afterEach(() => {
+ if (wrapper.destroy) {
+ wrapper.destroy();
+ }
+ axios.patch.mockReset();
+ refreshCurrentPage.mockReset();
+ createFlash.mockReset();
+ });
+
+ it('renders header text', () => {
+ mountComponent();
+ expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard');
+ });
+
+ describe('expand/collapse button', () => {
+ it('renders as an expand button by default', () => {
+ const button = wrapper.find(GlButton);
+
+ expect(button.text()).toBe('Expand');
+ });
+ });
+
+ describe('sub-header', () => {
+ let subHeader;
+
+ beforeEach(() => {
+ mountComponent();
+ subHeader = wrapper.find('.js-section-sub-header');
+ });
+
+ it('renders descriptive text', () => {
+ expect(subHeader.text()).toContain(
+ 'Add a button to the metrics dashboard linking directly to your existing external dashboards.',
+ );
+ });
+
+ it('renders help page link', () => {
+ const link = subHeader.find(GlLink);
+
+ expect(link.text()).toBe('Learn more');
+ expect(link.attributes().href).toBe(externalDashboardHelpPagePath);
+ });
+ });
+
+ describe('form', () => {
+ describe('input label', () => {
+ let formGroup;
+
+ beforeEach(() => {
+ mountComponent();
+ formGroup = wrapper.find(GlFormGroup);
+ });
+
+ it('uses label text', () => {
+ expect(formGroup.attributes().label).toBe('Full dashboard URL');
+ });
+
+ it('uses description text', () => {
+ expect(formGroup.attributes().description).toBe(
+ 'Enter the URL of the dashboard you want to link to',
+ );
+ });
+ });
+
+ describe('input field', () => {
+ let input;
+
+ beforeEach(() => {
+ mountComponent();
+ input = wrapper.find(GlFormInput);
+ });
+
+ it('defaults to externalDashboardUrl', () => {
+ expect(input.attributes().value).toBe(externalDashboardUrl);
+ });
+
+ it('uses a placeholder', () => {
+ expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards');
+ });
+ });
+
+ describe('submit button', () => {
+ const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
+
+ const endpointRequest = [
+ operationsSettingsEndpoint,
+ {
+ project: {
+ metrics_setting_attributes: {
+ external_dashboard_url: externalDashboardUrl,
+ },
+ },
+ },
+ ];
+
+ it('renders button label', () => {
+ mountComponent();
+ const submit = findSubmitButton();
+ expect(submit.text()).toBe('Save Changes');
+ });
+
+ it('submits form on click', () => {
+ mountComponent(false);
+ axios.patch.mockResolvedValue();
+ findSubmitButton().trigger('click');
+
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
+
+ return wrapper.vm.$nextTick().then(() => expect(refreshCurrentPage).toHaveBeenCalled());
+ });
+
+ it('creates flash banner on error', () => {
+ mountComponent(false);
+ const message = 'mockErrorMessage';
+ axios.patch.mockRejectedValue({ response: { data: { message } } });
+ findSubmitButton().trigger('click');
+
+ expect(axios.patch).toHaveBeenCalledWith(...endpointRequest);
+
+ return wrapper.vm
+ .$nextTick()
+ .then(jest.runAllTicks)
+ .then(() =>
+ expect(createFlash).toHaveBeenCalledWith(
+ `There was an error saving your changes. ${message}`,
+ 'alert',
+ ),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/operation_settings/store/mutations_spec.js b/spec/frontend/operation_settings/store/mutations_spec.js
new file mode 100644
index 00000000000..1854142c89a
--- /dev/null
+++ b/spec/frontend/operation_settings/store/mutations_spec.js
@@ -0,0 +1,19 @@
+import mutations from '~/operation_settings/store/mutations';
+import createState from '~/operation_settings/store/state';
+
+describe('operation settings mutations', () => {
+ let localState;
+
+ beforeEach(() => {
+ localState = createState();
+ });
+
+ describe('SET_EXTERNAL_DASHBOARD_URL', () => {
+ it('sets externalDashboardUrl', () => {
+ const mockUrl = 'mockUrl';
+ mutations.SET_EXTERNAL_DASHBOARD_URL(localState, mockUrl);
+
+ expect(localState.externalDashboardUrl).toBe(mockUrl);
+ });
+ });
+});
diff --git a/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
index 23d07056925..7e9aec84016 100644
--- a/spec/javascripts/pages/admin/abuse_reports/abuse_reports_spec.js
+++ b/spec/frontend/pages/admin/abuse_reports/abuse_reports_spec.js
@@ -3,7 +3,7 @@ import '~/lib/utils/text_utility';
import AbuseReports from '~/pages/admin/abuse_reports/abuse_reports';
describe('Abuse Reports', () => {
- const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
+ const FIXTURE = 'abuse_reports/abuse_reports_list.html';
const MAX_MESSAGE_LENGTH = 500;
let $messages;
@@ -16,23 +16,23 @@ describe('Abuse Reports', () => {
preloadFixtures(FIXTURE);
- beforeEach(function() {
+ beforeEach(() => {
loadFixtures(FIXTURE);
- this.abuseReports = new AbuseReports();
+ new AbuseReports(); // eslint-disable-line no-new
$messages = $('.abuse-reports .message');
});
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
- expect($longMessage.data('originalMessage')).toEqual(jasmine.anything());
+ expect($longMessage.data('originalMessage')).toEqual(expect.anything());
assertMaxLength($longMessage);
});
it('should not truncate short messages', () => {
const $shortMessage = findMessage('SHORT MESSAGE');
- expect($shortMessage.data('originalMessage')).not.toEqual(jasmine.anything());
+ expect($shortMessage.data('originalMessage')).not.toEqual(expect.anything());
});
it('should allow clicking a truncated message to expand and collapse the full message', () => {
diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
index efc338b36eb..6ac1e83829f 100644
--- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js
+++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
@@ -13,7 +13,7 @@ describe('EmojiMenu', () => {
let dummyEmojiList;
beforeEach(() => {
- dummySelectEmojiCallback = jasmine.createSpy('dummySelectEmojiCallback');
+ dummySelectEmojiCallback = jest.fn().mockName('dummySelectEmojiCallback');
dummyEmojiList = {
glEmojiTag() {
return dummyEmojiTag;
@@ -75,19 +75,19 @@ describe('EmojiMenu', () => {
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'one',
- jasmine.anything(),
+ expect.anything(),
'mouseenter focus',
dummyToggleButtonSelector,
'mouseenter focus',
- jasmine.anything(),
+ expect.anything(),
);
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
- jasmine.anything(),
+ expect.anything(),
'click',
dummyToggleButtonSelector,
- jasmine.anything(),
+ expect.anything(),
);
});
@@ -96,10 +96,10 @@ describe('EmojiMenu', () => {
expect(emojiMenu.registerEventListener).toHaveBeenCalledWith(
'on',
- jasmine.anything(),
+ expect.anything(),
'click',
`.js-awards-block .js-emoji-btn, .${dummyMenuClass} .js-emoji-btn`,
- jasmine.anything(),
+ expect.anything(),
);
});
});
diff --git a/spec/javascripts/performance_bar/services/performance_bar_service_spec.js b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
index cfec4b779e4..cfec4b779e4 100644
--- a/spec/javascripts/performance_bar/services/performance_bar_service_spec.js
+++ b/spec/frontend/performance_bar/services/performance_bar_service_spec.js
diff --git a/spec/javascripts/pipelines/blank_state_spec.js b/spec/frontend/pipelines/blank_state_spec.js
index 033bd5ccb73..033bd5ccb73 100644
--- a/spec/javascripts/pipelines/blank_state_spec.js
+++ b/spec/frontend/pipelines/blank_state_spec.js
diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/frontend/pipelines/empty_state_spec.js
index f12950b8fce..f12950b8fce 100644
--- a/spec/javascripts/pipelines/empty_state_spec.js
+++ b/spec/frontend/pipelines/empty_state_spec.js
diff --git a/spec/javascripts/pipelines/pipeline_store_spec.js b/spec/frontend/pipelines/pipeline_store_spec.js
index 1d5754d1f05..1d5754d1f05 100644
--- a/spec/javascripts/pipelines/pipeline_store_spec.js
+++ b/spec/frontend/pipelines/pipeline_store_spec.js
diff --git a/spec/javascripts/pipelines/pipelines_store_spec.js b/spec/frontend/pipelines/pipelines_store_spec.js
index ce21f788ed5..ce21f788ed5 100644
--- a/spec/javascripts/pipelines/pipelines_store_spec.js
+++ b/spec/frontend/pipelines/pipelines_store_spec.js
diff --git a/spec/javascripts/registry/getters_spec.js b/spec/frontend/registry/getters_spec.js
index 839aa718997..839aa718997 100644
--- a/spec/javascripts/registry/getters_spec.js
+++ b/spec/frontend/registry/getters_spec.js
diff --git a/spec/frontend/reports/components/report_item_spec.js b/spec/frontend/reports/components/report_item_spec.js
new file mode 100644
index 00000000000..bacbb399513
--- /dev/null
+++ b/spec/frontend/reports/components/report_item_spec.js
@@ -0,0 +1,33 @@
+import { shallowMount } from '@vue/test-utils';
+import { STATUS_SUCCESS } from '~/reports/constants';
+import ReportItem from '~/reports/components/report_item.vue';
+import { componentNames } from '~/reports/components/issue_body';
+
+describe('ReportItem', () => {
+ describe('showReportSectionStatusIcon', () => {
+ it('does not render CI Status Icon when showReportSectionStatusIcon is false', () => {
+ const wrapper = shallowMount(ReportItem, {
+ propsData: {
+ issue: { foo: 'bar' },
+ component: componentNames.TestIssueBody,
+ status: STATUS_SUCCESS,
+ showReportSectionStatusIcon: false,
+ },
+ });
+
+ expect(wrapper.find('issuestatusicon-stub').exists()).toBe(false);
+ });
+
+ it('shows status icon when unspecified', () => {
+ const wrapper = shallowMount(ReportItem, {
+ propsData: {
+ issue: { foo: 'bar' },
+ component: componentNames.TestIssueBody,
+ status: STATUS_SUCCESS,
+ },
+ });
+
+ expect(wrapper.find('issuestatusicon-stub').exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/reports/components/report_link_spec.js b/spec/frontend/reports/components/report_link_spec.js
index f879899e9c5..f879899e9c5 100644
--- a/spec/javascripts/reports/components/report_link_spec.js
+++ b/spec/frontend/reports/components/report_link_spec.js
diff --git a/spec/javascripts/reports/components/report_section_spec.js b/spec/frontend/reports/components/report_section_spec.js
index b02af8baaec..d4a3073374a 100644
--- a/spec/javascripts/reports/components/report_section_spec.js
+++ b/spec/frontend/reports/components/report_section_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import reportSection from '~/reports/components/report_section.vue';
-import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
+import mountComponent, { mountComponentWithSlots } from 'helpers/vue_mount_component_helper';
describe('Report section', () => {
let vm;
@@ -197,4 +197,44 @@ describe('Report section', () => {
expect(vm.$el.querySelector('.js-collapse-btn').textContent.trim()).toEqual('Expand');
});
});
+
+ describe('Success and Error slots', () => {
+ const createComponent = status => {
+ vm = mountComponentWithSlots(ReportSection, {
+ props: {
+ status,
+ hasIssues: true,
+ },
+ slots: {
+ success: ['This is a success'],
+ loading: ['This is loading'],
+ error: ['This is an error'],
+ },
+ });
+ };
+
+ it('only renders success slot when status is "SUCCESS"', () => {
+ createComponent('SUCCESS');
+
+ expect(vm.$el.textContent.trim()).toContain('This is a success');
+ expect(vm.$el.textContent.trim()).not.toContain('This is an error');
+ expect(vm.$el.textContent.trim()).not.toContain('This is loading');
+ });
+
+ it('only renders error slot when status is "ERROR"', () => {
+ createComponent('ERROR');
+
+ expect(vm.$el.textContent.trim()).toContain('This is an error');
+ expect(vm.$el.textContent.trim()).not.toContain('This is a success');
+ expect(vm.$el.textContent.trim()).not.toContain('This is loading');
+ });
+
+ it('only renders loading slot when status is "LOADING"', () => {
+ createComponent('LOADING');
+
+ expect(vm.$el.textContent.trim()).toContain('This is loading');
+ expect(vm.$el.textContent.trim()).not.toContain('This is an error');
+ expect(vm.$el.textContent.trim()).not.toContain('This is a success');
+ });
+ });
});
diff --git a/spec/javascripts/reports/store/utils_spec.js b/spec/frontend/reports/store/utils_spec.js
index 1679d120db2..1679d120db2 100644
--- a/spec/javascripts/reports/store/utils_spec.js
+++ b/spec/frontend/reports/store/utils_spec.js
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
new file mode 100644
index 00000000000..068fa317a87
--- /dev/null
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -0,0 +1,44 @@
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
+
+let vm;
+
+function factory(currentPath) {
+ vm = shallowMount(Breadcrumbs, {
+ propsData: {
+ currentPath,
+ },
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ });
+}
+
+describe('Repository breadcrumbs component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it.each`
+ path | linkCount
+ ${'/'} | ${1}
+ ${'app'} | ${2}
+ ${'app/assets'} | ${3}
+ ${'app/assets/javascripts'} | ${4}
+ `('renders $linkCount links for path $path', ({ path, linkCount }) => {
+ factory(path);
+
+ expect(vm.findAll(RouterLinkStub).length).toEqual(linkCount);
+ });
+
+ it('renders last link as active', () => {
+ factory('app/assets');
+
+ expect(
+ vm
+ .findAll(RouterLinkStub)
+ .at(2)
+ .attributes('aria-current'),
+ ).toEqual('page');
+ });
+});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
new file mode 100644
index 00000000000..1b4564303e4
--- /dev/null
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Repository table row component renders table row 1`] = `
+<tr
+ class="tree-item file_1"
+>
+ <td
+ class="tree-item-file-name"
+ >
+ <i
+ aria-label="file"
+ class="fa fa-fw fa-file-text-o"
+ role="img"
+ />
+
+ <a
+ class="str-truncated"
+ >
+
+ test
+
+ </a>
+
+ <!---->
+ </td>
+
+ <td
+ class="d-none d-sm-table-cell tree-commit"
+ />
+
+ <td
+ class="tree-time-ago text-right"
+ />
+</tr>
+`;
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
new file mode 100644
index 00000000000..827927e6d9a
--- /dev/null
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -0,0 +1,80 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Table from '~/repository/components/table/index.vue';
+
+let vm;
+let $apollo;
+
+function factory(path, data = () => ({})) {
+ $apollo = {
+ query: jest.fn().mockReturnValue(Promise.resolve({ data: data() })),
+ };
+
+ vm = shallowMount(Table, {
+ propsData: {
+ path,
+ },
+ mocks: {
+ $apollo,
+ },
+ });
+}
+
+describe('Repository table component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it.each`
+ path | ref
+ ${'/'} | ${'master'}
+ ${'app/assets'} | ${'master'}
+ ${'/'} | ${'test'}
+ `('renders table caption for $ref in $path', ({ path, ref }) => {
+ factory(path);
+
+ vm.setData({ ref });
+
+ expect(vm.find('caption').text()).toEqual(
+ `Files, directories, and submodules in the path ${path} for commit reference ${ref}`,
+ );
+ });
+
+ it('shows loading icon', () => {
+ factory('/');
+
+ vm.setData({ isLoadingFiles: true });
+
+ expect(vm.find(GlLoadingIcon).isVisible()).toBe(true);
+ });
+
+ describe('normalizeData', () => {
+ it('normalizes edge nodes', () => {
+ const output = vm.vm.normalizeData('blobs', [{ node: '1' }, { node: '2' }]);
+
+ expect(output).toEqual(['1', '2']);
+ });
+ });
+
+ describe('hasNextPage', () => {
+ it('returns undefined when hasNextPage is false', () => {
+ const output = vm.vm.hasNextPage({
+ trees: { pageInfo: { hasNextPage: false } },
+ submodules: { pageInfo: { hasNextPage: false } },
+ blobs: { pageInfo: { hasNextPage: false } },
+ });
+
+ expect(output).toBe(undefined);
+ });
+
+ it('returns pageInfo object when hasNextPage is true', () => {
+ const output = vm.vm.hasNextPage({
+ trees: { pageInfo: { hasNextPage: false } },
+ submodules: { pageInfo: { hasNextPage: false } },
+ blobs: { pageInfo: { hasNextPage: true, nextCursor: 'test' } },
+ });
+
+ expect(output).toEqual({ hasNextPage: true, nextCursor: 'test' });
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/table/parent_row_spec.js b/spec/frontend/repository/components/table/parent_row_spec.js
new file mode 100644
index 00000000000..7020055271f
--- /dev/null
+++ b/spec/frontend/repository/components/table/parent_row_spec.js
@@ -0,0 +1,64 @@
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import ParentRow from '~/repository/components/table/parent_row.vue';
+
+let vm;
+let $router;
+
+function factory(path) {
+ $router = {
+ push: jest.fn(),
+ };
+
+ vm = shallowMount(ParentRow, {
+ propsData: {
+ commitRef: 'master',
+ path,
+ },
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ mocks: {
+ $router,
+ },
+ });
+}
+
+describe('Repository parent row component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it.each`
+ path | to
+ ${'app'} | ${'/tree/master/'}
+ ${'app/assets'} | ${'/tree/master/app'}
+ `('renders link in $path to $to', ({ path, to }) => {
+ factory(path);
+
+ expect(vm.find(RouterLinkStub).props().to).toEqual({
+ path: to,
+ });
+ });
+
+ it('pushes new router when clicking row', () => {
+ factory('app/assets');
+
+ vm.find('td').trigger('click');
+
+ expect($router.push).toHaveBeenCalledWith({
+ path: '/tree/master/app',
+ });
+ });
+
+ // We test that it does not get called when clicking any internal
+ // links as this was causing multipe routes to get pushed
+ it('does not trigger router.push when clicking link', () => {
+ factory('app/assets');
+
+ vm.find('a').trigger('click');
+
+ expect($router.push).not.toHaveBeenCalledWith({
+ path: '/tree/master/app',
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
new file mode 100644
index 00000000000..a70dc7bb866
--- /dev/null
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -0,0 +1,101 @@
+import { shallowMount, RouterLinkStub } from '@vue/test-utils';
+import TableRow from '~/repository/components/table/row.vue';
+
+let vm;
+let $router;
+
+function factory(propsData = {}) {
+ $router = {
+ push: jest.fn(),
+ };
+
+ vm = shallowMount(TableRow, {
+ propsData,
+ mocks: {
+ $router,
+ },
+ stubs: {
+ RouterLink: RouterLinkStub,
+ },
+ });
+
+ vm.setData({ ref: 'master' });
+}
+
+describe('Repository table row component', () => {
+ afterEach(() => {
+ vm.destroy();
+ });
+
+ it('renders table row', () => {
+ factory({
+ id: '1',
+ path: 'test',
+ type: 'file',
+ currentPath: '/',
+ });
+
+ expect(vm.element).toMatchSnapshot();
+ });
+
+ it.each`
+ type | component | componentName
+ ${'tree'} | ${RouterLinkStub} | ${'RouterLink'}
+ ${'file'} | ${'a'} | ${'hyperlink'}
+ ${'commit'} | ${'a'} | ${'hyperlink'}
+ `('renders a $componentName for type $type', ({ type, component }) => {
+ factory({
+ id: '1',
+ path: 'test',
+ type,
+ currentPath: '/',
+ });
+
+ expect(vm.find(component).exists()).toBe(true);
+ });
+
+ it.each`
+ type | pushes
+ ${'tree'} | ${true}
+ ${'file'} | ${false}
+ ${'commit'} | ${false}
+ `('pushes new router if type $type is tree', ({ type, pushes }) => {
+ factory({
+ id: '1',
+ path: 'test',
+ type,
+ currentPath: '/',
+ });
+
+ vm.trigger('click');
+
+ if (pushes) {
+ expect($router.push).toHaveBeenCalledWith({ path: '/tree/master/test' });
+ } else {
+ expect($router.push).not.toHaveBeenCalled();
+ }
+ });
+
+ it('renders commit ID for submodule', () => {
+ factory({
+ id: '1',
+ path: 'test',
+ type: 'commit',
+ currentPath: '/',
+ });
+
+ expect(vm.find('.commit-sha').text()).toContain('1');
+ });
+
+ it('renders link with href', () => {
+ factory({
+ id: '1',
+ path: 'test',
+ type: 'blob',
+ url: 'https://test.com',
+ currentPath: '/',
+ });
+
+ expect(vm.find('a').attributes('href')).toEqual('https://test.com');
+ });
+});
diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js
new file mode 100644
index 00000000000..f61a0ccd1e6
--- /dev/null
+++ b/spec/frontend/repository/router_spec.js
@@ -0,0 +1,23 @@
+import IndexPage from '~/repository/pages/index.vue';
+import TreePage from '~/repository/pages/tree.vue';
+import createRouter from '~/repository/router';
+
+describe('Repository router spec', () => {
+ it.each`
+ path | component | componentName
+ ${'/'} | ${IndexPage} | ${'IndexPage'}
+ ${'/tree/master'} | ${TreePage} | ${'TreePage'}
+ ${'/tree/master/app/assets'} | ${TreePage} | ${'TreePage'}
+ ${'/tree/123/app/assets'} | ${null} | ${'null'}
+ `('sets component as $componentName for path "$path"', ({ path, component }) => {
+ const router = createRouter('', 'master');
+
+ const componentsForRoute = router.getMatchedComponents(path);
+
+ expect(componentsForRoute.length).toBe(component ? 1 : 0);
+
+ if (component) {
+ expect(componentsForRoute).toContain(component);
+ }
+ });
+});
diff --git a/spec/frontend/repository/utils/icon_spec.js b/spec/frontend/repository/utils/icon_spec.js
new file mode 100644
index 00000000000..3d84705f7ea
--- /dev/null
+++ b/spec/frontend/repository/utils/icon_spec.js
@@ -0,0 +1,23 @@
+import { getIconName } from '~/repository/utils/icon';
+
+describe('getIconName', () => {
+ // Tests the returning font awesome icon name
+ // We only test one for each file type to save testing a lot of different
+ // file types
+ it.each`
+ type | path | icon
+ ${'tree'} | ${''} | ${'folder'}
+ ${'commit'} | ${''} | ${'archive'}
+ ${'file'} | ${'test.pdf'} | ${'file-pdf-o'}
+ ${'file'} | ${'test.jpg'} | ${'file-image-o'}
+ ${'file'} | ${'test.zip'} | ${'file-archive-o'}
+ ${'file'} | ${'test.mp3'} | ${'file-audio-o'}
+ ${'file'} | ${'test.flv'} | ${'file-video-o'}
+ ${'file'} | ${'test.dotx'} | ${'file-word-o'}
+ ${'file'} | ${'test.xlsb'} | ${'file-excel-o'}
+ ${'file'} | ${'test.ppam'} | ${'file-powerpoint-o'}
+ ${'file'} | ${'test.js'} | ${'file-text-o'}
+ `('returns $icon for $type with path $path', ({ type, path, icon }) => {
+ expect(getIconName(type, path)).toEqual(icon);
+ });
+});
diff --git a/spec/frontend/repository/utils/title_spec.js b/spec/frontend/repository/utils/title_spec.js
new file mode 100644
index 00000000000..c4879716fd7
--- /dev/null
+++ b/spec/frontend/repository/utils/title_spec.js
@@ -0,0 +1,15 @@
+import { setTitle } from '~/repository/utils/title';
+
+describe('setTitle', () => {
+ it.each`
+ path | title
+ ${'/'} | ${'Files'}
+ ${'app'} | ${'app'}
+ ${'app/assets'} | ${'app/assets'}
+ ${'app/assets/javascripts'} | ${'app/assets/javascripts'}
+ `('sets document title as $title for $path', ({ path, title }) => {
+ setTitle(path, 'master', 'GitLab');
+
+ expect(document.title).toEqual(`${title} · master · GitLab`);
+ });
+});
diff --git a/spec/frontend/serverless/components/area_spec.js b/spec/frontend/serverless/components/area_spec.js
new file mode 100644
index 00000000000..62005e1981a
--- /dev/null
+++ b/spec/frontend/serverless/components/area_spec.js
@@ -0,0 +1,122 @@
+import { shallowMount } from '@vue/test-utils';
+import Area from '~/serverless/components/area.vue';
+import { mockNormalizedMetrics } from '../mock_data';
+
+describe('Area component', () => {
+ const mockWidgets = 'mockWidgets';
+ const mockGraphData = mockNormalizedMetrics;
+ let areaChart;
+
+ beforeEach(() => {
+ areaChart = shallowMount(Area, {
+ propsData: {
+ graphData: mockGraphData,
+ containerWidth: 0,
+ },
+ slots: {
+ default: mockWidgets,
+ },
+ sync: false,
+ });
+ });
+
+ afterEach(() => {
+ areaChart.destroy();
+ });
+
+ it('renders chart title', () => {
+ expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title);
+ });
+
+ it('contains graph widgets from slot', () => {
+ expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets);
+ });
+
+ describe('methods', () => {
+ describe('formatTooltipText', () => {
+ const mockDate = mockNormalizedMetrics.queries[0].result[0].values[0].time;
+ const generateSeriesData = type => ({
+ seriesData: [
+ {
+ componentSubType: type,
+ value: [mockDate, 4],
+ },
+ ],
+ value: mockDate,
+ });
+
+ describe('series is of line type', () => {
+ beforeEach(() => {
+ areaChart.vm.formatTooltipText(generateSeriesData('line'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(areaChart.vm.tooltipPopoverTitle).toBe('28 Feb 2019, 11:11AM');
+ });
+
+ it('formats tooltip content', () => {
+ expect(areaChart.vm.tooltipPopoverContent).toBe('Invocations (requests): 4');
+ });
+ });
+
+ it('verify default interval value of 1', () => {
+ expect(areaChart.vm.getInterval).toBe(1);
+ });
+ });
+
+ describe('onResize', () => {
+ const mockWidth = 233;
+
+ beforeEach(() => {
+ jest.spyOn(Element.prototype, 'getBoundingClientRect').mockImplementation(() => ({
+ width: mockWidth,
+ }));
+ areaChart.vm.onResize();
+ });
+
+ it('sets area chart width', () => {
+ expect(areaChart.vm.width).toBe(mockWidth);
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('chartData', () => {
+ it('utilizes all data points', () => {
+ expect(Object.keys(areaChart.vm.chartData)).toEqual(['requests']);
+ expect(areaChart.vm.chartData.requests.length).toBe(2);
+ });
+
+ it('creates valid data', () => {
+ const data = areaChart.vm.chartData.requests;
+
+ expect(
+ data.filter(
+ datum => new Date(datum.time).getTime() > 0 && typeof datum.value === 'number',
+ ).length,
+ ).toBe(data.length);
+ });
+ });
+
+ describe('generateSeries', () => {
+ it('utilizes correct time data', () => {
+ expect(areaChart.vm.generateSeries.data).toEqual([
+ ['2019-02-28T11:11:38.756Z', 0],
+ ['2019-02-28T11:12:38.756Z', 0],
+ ]);
+ });
+ });
+
+ describe('xAxisLabel', () => {
+ it('constructs a label for the chart x-axis', () => {
+ expect(areaChart.vm.xAxisLabel).toBe('invocations / minute');
+ });
+ });
+
+ describe('yAxisLabel', () => {
+ it('constructs a label for the chart y-axis', () => {
+ expect(areaChart.vm.yAxisLabel).toBe('Invocations (requests)');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js
index bdf7a714910..0ad85e218dc 100644
--- a/spec/javascripts/serverless/components/environment_row_spec.js
+++ b/spec/frontend/serverless/components/environment_row_spec.js
@@ -1,81 +1,74 @@
-import Vue from 'vue';
-
import environmentRowComponent from '~/serverless/components/environment_row.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ServerlessStore from '~/serverless/stores/serverless_store';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
+import { translate } from '~/serverless/utils';
-const createComponent = (env, envName) =>
- mountComponent(Vue.extend(environmentRowComponent), { env, envName });
+const createComponent = (localVue, env, envName) =>
+ shallowMount(environmentRowComponent, { localVue, propsData: { env, envName }, sync: false }).vm;
describe('environment row component', () => {
describe('default global cluster case', () => {
+ let localVue;
let vm;
beforeEach(() => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctions);
- vm = createComponent(store.state.functions['*'], '*');
+ localVue = createLocalVue();
+ vm = createComponent(localVue, translate(mockServerlessFunctions.functions)['*'], '*');
});
+ afterEach(() => vm.$destroy());
+
it('has the correct envId', () => {
expect(vm.envId).toEqual('env-global');
- vm.$destroy();
});
it('is open by default', () => {
expect(vm.isOpenClass).toEqual({ 'is-open': true });
- vm.$destroy();
});
it('generates correct output', () => {
- expect(vm.$el.querySelectorAll('li').length).toEqual(2);
expect(vm.$el.id).toEqual('env-global');
expect(vm.$el.classList.contains('is-open')).toBe(true);
expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('*');
-
- vm.$destroy();
});
it('opens and closes correctly', () => {
expect(vm.isOpen).toBe(true);
vm.toggleOpen();
- Vue.nextTick(() => {
- expect(vm.isOpen).toBe(false);
- });
- vm.$destroy();
+ expect(vm.isOpen).toBe(false);
});
});
describe('default named cluster case', () => {
let vm;
+ let localVue;
beforeEach(() => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctionsDiffEnv);
- vm = createComponent(store.state.functions.test, 'test');
+ localVue = createLocalVue();
+ vm = createComponent(
+ localVue,
+ translate(mockServerlessFunctionsDiffEnv.functions).test,
+ 'test',
+ );
});
+ afterEach(() => vm.$destroy());
+
it('has the correct envId', () => {
expect(vm.envId).toEqual('env-test');
- vm.$destroy();
});
it('is open by default', () => {
expect(vm.isOpenClass).toEqual({ 'is-open': true });
- vm.$destroy();
});
it('generates correct output', () => {
- expect(vm.$el.querySelectorAll('li').length).toEqual(1);
expect(vm.$el.id).toEqual('env-test');
expect(vm.$el.classList.contains('is-open')).toBe(true);
expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('test');
-
- vm.$destroy();
});
});
});
diff --git a/spec/frontend/serverless/components/function_details_spec.js b/spec/frontend/serverless/components/function_details_spec.js
new file mode 100644
index 00000000000..31348ff1194
--- /dev/null
+++ b/spec/frontend/serverless/components/function_details_spec.js
@@ -0,0 +1,117 @@
+import Vuex from 'vuex';
+
+import functionDetailsComponent from '~/serverless/components/function_details.vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
+
+describe('functionDetailsComponent', () => {
+ let localVue;
+ let component;
+ let store;
+
+ beforeEach(() => {
+ localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ store = createStore();
+ });
+
+ afterEach(() => {
+ component.vm.$destroy();
+ });
+
+ describe('Verify base functionality', () => {
+ const serviceStub = {
+ name: 'test',
+ description: 'a description',
+ environment: '*',
+ url: 'http://service.com/test',
+ namespace: 'test-ns',
+ podcount: 0,
+ metricsUrl: '/metrics',
+ };
+
+ it('has a name, description, URL, and no pods loaded', () => {
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ sync: false,
+ });
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-name').innerHTML.trim(),
+ ).toContain('test');
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-description').innerHTML.trim(),
+ ).toContain('a description');
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain(
+ 'No pods loaded at this time.',
+ );
+ });
+
+ it('has a pods loaded', () => {
+ serviceStub.podcount = 1;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ sync: false,
+ });
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('1 pod in use');
+ });
+
+ it('has multiple pods loaded', () => {
+ serviceStub.podcount = 3;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ sync: false,
+ });
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('3 pods in use');
+ });
+
+ it('can support a missing description', () => {
+ serviceStub.description = null;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ sync: false,
+ });
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-description').querySelector('div')
+ .innerHTML.length,
+ ).toEqual(0);
+ });
+ });
+});
diff --git a/spec/frontend/serverless/components/function_row_spec.js b/spec/frontend/serverless/components/function_row_spec.js
new file mode 100644
index 00000000000..979f98c4832
--- /dev/null
+++ b/spec/frontend/serverless/components/function_row_spec.js
@@ -0,0 +1,32 @@
+import functionRowComponent from '~/serverless/components/function_row.vue';
+import { shallowMount } from '@vue/test-utils';
+import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import { mockServerlessFunction } from '../mock_data';
+
+describe('functionRowComponent', () => {
+ let wrapper;
+
+ const createComponent = func => {
+ wrapper = shallowMount(functionRowComponent, { propsData: { func }, sync: false });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('Parses the function details correctly', () => {
+ createComponent(mockServerlessFunction);
+
+ expect(wrapper.find('b').text()).toBe(mockServerlessFunction.name);
+ expect(wrapper.find('span').text()).toBe(mockServerlessFunction.image);
+ expect(wrapper.find(Timeago).attributes('time')).not.toBe(null);
+ });
+
+ it('handles clicks correctly', () => {
+ createComponent(mockServerlessFunction);
+ const { vm } = wrapper;
+
+ expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row
+ });
+});
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
new file mode 100644
index 00000000000..d8a80f8031e
--- /dev/null
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -0,0 +1,130 @@
+import Vuex from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import functionsComponent from '~/serverless/components/functions.vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
+import EmptyState from '~/serverless/components/empty_state.vue';
+import EnvironmentRow from '~/serverless/components/environment_row.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import { mockServerlessFunctions } from '../mock_data';
+
+describe('functionsComponent', () => {
+ const statusPath = `${TEST_HOST}/statusPath`;
+
+ let component;
+ let store;
+ let localVue;
+ let axiosMock;
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onGet(statusPath).reply(200);
+
+ localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ store = createStore();
+ });
+
+ afterEach(() => {
+ component.vm.$destroy();
+ axiosMock.restore();
+ });
+
+ it('should render empty state when Knative is not installed', () => {
+ store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(component.find(EmptyState).exists()).toBe(true);
+ });
+
+ it('should render a loading component', () => {
+ store.dispatch('requestFunctionsLoading');
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(component.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('should render empty state when there is no function data', () => {
+ store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(
+ component.vm.$el
+ .querySelector('.empty-state, .js-empty-state')
+ .classList.contains('js-empty-state'),
+ ).toBe(true);
+
+ expect(component.vm.$el.querySelector('.state-title, .text-center').innerHTML.trim()).toEqual(
+ 'No functions available',
+ );
+ });
+
+ it('should render functions and a loader when functions are partially fetched', () => {
+ store.dispatch('receiveFunctionsPartial', {
+ ...mockServerlessFunctions,
+ knative_installed: 'checking',
+ });
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ expect(component.find('.js-functions-wrapper').exists()).toBe(true);
+ expect(component.find('.js-functions-loader').exists()).toBe(true);
+ });
+
+ it('should render the functions list', () => {
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ clustersPath: 'clustersPath',
+ helpPath: 'helpPath',
+ statusPath,
+ },
+ sync: false,
+ });
+
+ component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
+
+ return component.vm.$nextTick().then(() => {
+ expect(component.find(EnvironmentRow).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js
new file mode 100644
index 00000000000..5dbdccde2de
--- /dev/null
+++ b/spec/frontend/serverless/components/missing_prometheus_spec.js
@@ -0,0 +1,41 @@
+import { GlButton } from '@gitlab/ui';
+import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createComponent = missingData =>
+ shallowMount(missingPrometheusComponent, {
+ propsData: {
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ missingData,
+ },
+ sync: false,
+ });
+
+describe('missingPrometheusComponent', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render missing prometheus message', () => {
+ wrapper = createComponent(false);
+ const { vm } = wrapper;
+
+ expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
+ 'Function invocation metrics require Prometheus to be installed first.',
+ );
+
+ expect(wrapper.find(GlButton).attributes('variant')).toBe('success');
+ });
+
+ it('should render no prometheus data message', () => {
+ wrapper = createComponent(true);
+ const { vm } = wrapper;
+
+ expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
+ 'Invocation metrics loading or not available at this time.',
+ );
+ });
+});
diff --git a/spec/frontend/serverless/components/pod_box_spec.js b/spec/frontend/serverless/components/pod_box_spec.js
new file mode 100644
index 00000000000..d82825d8f62
--- /dev/null
+++ b/spec/frontend/serverless/components/pod_box_spec.js
@@ -0,0 +1,23 @@
+import podBoxComponent from '~/serverless/components/pod_box.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createComponent = count =>
+ shallowMount(podBoxComponent, {
+ propsData: {
+ count,
+ },
+ sync: false,
+ }).vm;
+
+describe('podBoxComponent', () => {
+ it('should render three boxes', () => {
+ const count = 3;
+ const vm = createComponent(count);
+ const rects = vm.$el.querySelectorAll('rect');
+
+ expect(rects.length).toEqual(3);
+ expect(parseInt(rects[2].getAttribute('x'), 10)).toEqual(40);
+
+ vm.$destroy();
+ });
+});
diff --git a/spec/javascripts/serverless/components/url_spec.js b/spec/frontend/serverless/components/url_spec.js
index 21a879a49bb..706441e8a8b 100644
--- a/spec/javascripts/serverless/components/url_spec.js
+++ b/spec/frontend/serverless/components/url_spec.js
@@ -1,25 +1,24 @@
import Vue from 'vue';
-
import urlComponent from '~/serverless/components/url.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = uri => {
- const component = Vue.extend(urlComponent);
+import { shallowMount } from '@vue/test-utils';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
- return mountComponent(component, {
- uri,
+const createComponent = uri =>
+ shallowMount(Vue.extend(urlComponent), {
+ propsData: {
+ uri,
+ },
+ sync: false,
});
-};
describe('urlComponent', () => {
it('should render correctly', () => {
const uri = 'http://testfunc.apps.example.com';
- const vm = createComponent(uri);
+ const wrapper = createComponent(uri);
+ const { vm } = wrapper;
expect(vm.$el.classList.contains('clipboard-group')).toBe(true);
- expect(vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text')).toEqual(
- uri,
- );
+ expect(wrapper.find(ClipboardButton).attributes('text')).toEqual(uri);
expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri);
diff --git a/spec/frontend/serverless/mock_data.js b/spec/frontend/serverless/mock_data.js
new file mode 100644
index 00000000000..ef616ceb37f
--- /dev/null
+++ b/spec/frontend/serverless/mock_data.js
@@ -0,0 +1,142 @@
+export const mockServerlessFunctions = {
+ knative_installed: true,
+ functions: [
+ {
+ name: 'testfunc1',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc1.tm-example.apps.example.com',
+ description: 'A test service',
+ image: 'knative-test-container-buildtemplate',
+ },
+ {
+ name: 'testfunc2',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc2.tm-example.apps.example.com',
+ description: 'A second test service\nThis one with additional descriptions',
+ image: 'knative-test-echo-buildtemplate',
+ },
+ ],
+};
+
+export const mockServerlessFunctionsDiffEnv = {
+ knative_installed: true,
+ functions: [
+ {
+ name: 'testfunc1',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc1.tm-example.apps.example.com',
+ description: 'A test service',
+ image: 'knative-test-container-buildtemplate',
+ },
+ {
+ name: 'testfunc2',
+ namespace: 'tm-example',
+ environment_scope: 'test',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
+ podcount: null,
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc2.tm-example.apps.example.com',
+ description: 'A second test service\nThis one with additional descriptions',
+ image: 'knative-test-echo-buildtemplate',
+ },
+ ],
+};
+
+export const mockServerlessFunction = {
+ name: 'testfunc1',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+ podcount: '3',
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc1.tm-example.apps.example.com',
+ description: 'A test service',
+ image: 'knative-test-container-buildtemplate',
+};
+
+export const mockMultilineServerlessFunction = {
+ name: 'testfunc1',
+ namespace: 'tm-example',
+ environment_scope: '*',
+ cluster_id: 46,
+ detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
+ podcount: '3',
+ created_at: '2019-02-05T01:01:23Z',
+ url: 'http://testfunc1.tm-example.apps.example.com',
+ description: 'testfunc1\nA test service line\\nWith additional services',
+ image: 'knative-test-container-buildtemplate',
+};
+
+export const mockMetrics = {
+ success: true,
+ last_update: '2019-02-28T19:11:38.926Z',
+ metrics: {
+ id: 22,
+ title: 'Knative function invocations',
+ required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'],
+ weight: 0,
+ y_label: 'Invocations',
+ queries: [
+ {
+ query_range:
+ 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))',
+ unit: 'requests',
+ label: 'invocations / minute',
+ result: [
+ {
+ metric: {},
+ values: [[1551352298.756, '0'], [1551352358.756, '0']],
+ },
+ ],
+ },
+ ],
+ },
+};
+
+export const mockNormalizedMetrics = {
+ id: 22,
+ title: 'Knative function invocations',
+ required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'],
+ weight: 0,
+ y_label: 'Invocations',
+ queries: [
+ {
+ query_range:
+ 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))',
+ unit: 'requests',
+ label: 'invocations / minute',
+ result: [
+ {
+ metric: {},
+ values: [
+ {
+ time: '2019-02-28T11:11:38.756Z',
+ value: 0,
+ },
+ {
+ time: '2019-02-28T11:12:38.756Z',
+ value: 0,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
diff --git a/spec/frontend/serverless/store/actions_spec.js b/spec/frontend/serverless/store/actions_spec.js
new file mode 100644
index 00000000000..aac57c75a4f
--- /dev/null
+++ b/spec/frontend/serverless/store/actions_spec.js
@@ -0,0 +1,90 @@
+import MockAdapter from 'axios-mock-adapter';
+import statusCodes from '~/lib/utils/http_status';
+import { fetchFunctions, fetchMetrics } from '~/serverless/store/actions';
+import { mockServerlessFunctions, mockMetrics } from '../mock_data';
+import axios from '~/lib/utils/axios_utils';
+import testAction from '../../helpers/vuex_action_helper';
+import { adjustMetricQuery } from '../utils';
+
+describe('ServerlessActions', () => {
+ describe('fetchFunctions', () => {
+ it('should successfully fetch functions', done => {
+ const endpoint = '/functions';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockServerlessFunctions));
+
+ testAction(
+ fetchFunctions,
+ { functionsPath: endpoint },
+ {},
+ [],
+ [
+ { type: 'requestFunctionsLoading' },
+ { type: 'receiveFunctionsSuccess', payload: mockServerlessFunctions },
+ ],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+
+ it('should successfully retry', done => {
+ const endpoint = '/functions';
+ const mock = new MockAdapter(axios);
+ mock
+ .onGet(endpoint)
+ .reply(() => new Promise(resolve => setTimeout(() => resolve(200), Infinity)));
+
+ testAction(
+ fetchFunctions,
+ { functionsPath: endpoint },
+ {},
+ [],
+ [{ type: 'requestFunctionsLoading' }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('fetchMetrics', () => {
+ it('should return no prometheus', done => {
+ const endpoint = '/metrics';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.NO_CONTENT);
+
+ testAction(
+ fetchMetrics,
+ { metricsPath: endpoint, hasPrometheus: false },
+ {},
+ [],
+ [{ type: 'receiveMetricsNoPrometheus' }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+
+ it('should successfully fetch metrics', done => {
+ const endpoint = '/metrics';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockMetrics));
+
+ testAction(
+ fetchMetrics,
+ { metricsPath: endpoint, hasPrometheus: true },
+ {},
+ [],
+ [{ type: 'receiveMetricsSuccess', payload: adjustMetricQuery(mockMetrics) }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/serverless/store/getters_spec.js b/spec/frontend/serverless/store/getters_spec.js
new file mode 100644
index 00000000000..92853fda37c
--- /dev/null
+++ b/spec/frontend/serverless/store/getters_spec.js
@@ -0,0 +1,43 @@
+import serverlessState from '~/serverless/store/state';
+import * as getters from '~/serverless/store/getters';
+import { mockServerlessFunctions } from '../mock_data';
+
+describe('Serverless Store Getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = serverlessState;
+ });
+
+ describe('hasPrometheusMissingData', () => {
+ it('should return false if Prometheus is not installed', () => {
+ state.hasPrometheus = false;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(false);
+ });
+
+ it('should return false if Prometheus is installed and there is data', () => {
+ state.hasPrometheusData = true;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(false);
+ });
+
+ it('should return true if Prometheus is installed and there is no data', () => {
+ state.hasPrometheus = true;
+ state.hasPrometheusData = false;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(true);
+ });
+ });
+
+ describe('getFunctions', () => {
+ it('should translate the raw function array to group the functions per environment scope', () => {
+ state.functions = mockServerlessFunctions.functions;
+
+ const funcs = getters.getFunctions(state);
+
+ expect(Object.keys(funcs)).toContain('*');
+ expect(funcs['*'].length).toEqual(2);
+ });
+ });
+});
diff --git a/spec/frontend/serverless/store/mutations_spec.js b/spec/frontend/serverless/store/mutations_spec.js
new file mode 100644
index 00000000000..e2771c7e5fd
--- /dev/null
+++ b/spec/frontend/serverless/store/mutations_spec.js
@@ -0,0 +1,86 @@
+import mutations from '~/serverless/store/mutations';
+import * as types from '~/serverless/store/mutation_types';
+import { mockServerlessFunctions, mockMetrics } from '../mock_data';
+
+describe('ServerlessMutations', () => {
+ describe('Functions List Mutations', () => {
+ it('should ensure loading is true', () => {
+ const state = {};
+
+ mutations[types.REQUEST_FUNCTIONS_LOADING](state);
+
+ expect(state.isLoading).toEqual(true);
+ });
+
+ it('should set proper state once functions are loaded', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_SUCCESS](state, mockServerlessFunctions);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(true);
+ expect(state.functions).toEqual(mockServerlessFunctions.functions);
+ });
+
+ it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state, { knative_installed: true });
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(false);
+ expect(state.functions).toBe(undefined);
+ });
+
+ it('should ensure loading has stopped, and an error is raised', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_ERROR](state, 'sample error');
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(false);
+ expect(state.functions).toBe(undefined);
+ expect(state.error).not.toBe(undefined);
+ });
+ });
+
+ describe('Function Details Metrics Mutations', () => {
+ it('should ensure isLoading and hasPrometheus data flags indicate data is loaded', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_SUCCESS](state, mockMetrics);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(true);
+ expect(state.graphData).toEqual(mockMetrics);
+ });
+
+ it('should ensure isLoading and hasPrometheus data flags are cleared indicating no functions available', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_NODATA_SUCCESS](state);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(false);
+ expect(state.graphData).toBe(undefined);
+ });
+
+ it('should properly indicate an error', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_ERROR](state, 'sample error');
+
+ expect(state.hasPrometheusData).toEqual(false);
+ expect(state.error).not.toBe(undefined);
+ });
+
+ it('should properly indicate when prometheus is installed', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_NO_PROMETHEUS](state);
+
+ expect(state.hasPrometheus).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(false);
+ });
+ });
+});
diff --git a/spec/frontend/serverless/utils.js b/spec/frontend/serverless/utils.js
new file mode 100644
index 00000000000..5ce2e37d493
--- /dev/null
+++ b/spec/frontend/serverless/utils.js
@@ -0,0 +1,20 @@
+export const adjustMetricQuery = data => {
+ const updatedMetric = data.metrics;
+
+ const queries = data.metrics.queries.map(query => ({
+ ...query,
+ result: query.result.map(result => ({
+ ...result,
+ values: result.values.map(([timestamp, value]) => ({
+ time: new Date(timestamp * 1000).toISOString(),
+ value: Number(value),
+ })),
+ })),
+ }));
+
+ updatedMetric.queries = queries;
+ return updatedMetric;
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/spec/javascripts/sidebar/confidential_edit_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_buttons_spec.js
index 32da9f83112..32da9f83112 100644
--- a/spec/javascripts/sidebar/confidential_edit_buttons_spec.js
+++ b/spec/frontend/sidebar/confidential_edit_buttons_spec.js
diff --git a/spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
index 369088cb258..369088cb258 100644
--- a/spec/javascripts/sidebar/confidential_edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/confidential_edit_form_buttons_spec.js
diff --git a/spec/javascripts/sidebar/lock/edit_form_spec.js b/spec/frontend/sidebar/lock/edit_form_spec.js
index ec10a999a40..ec10a999a40 100644
--- a/spec/javascripts/sidebar/lock/edit_form_spec.js
+++ b/spec/frontend/sidebar/lock/edit_form_spec.js
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index d892889b98d..7e7cc1488b8 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -1,23 +1,21 @@
import Vue from 'vue';
+import * as jqueryMatchers from 'custom-jquery-matchers';
import Translate from '~/vue_shared/translate';
import axios from '~/lib/utils/axios_utils';
+import { initializeTestTimeout } from './helpers/timeout';
+import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
-const testTimeoutInMs = 300;
-jest.setTimeout(testTimeoutInMs);
+process.on('unhandledRejection', global.promiseRejectionHandler);
-let testStartTime;
+afterEach(() =>
+ // give Promises a bit more time so they fail the right test
+ new Promise(setImmediate).then(() => {
+ // wait for pending setTimeout()s
+ jest.runAllTimers();
+ }),
+);
-// https://github.com/facebook/jest/issues/6947
-beforeEach(() => {
- testStartTime = Date.now();
-});
-
-afterEach(() => {
- const elapsedTimeInMs = Date.now() - testStartTime;
- if (elapsedTimeInMs > testTimeoutInMs) {
- throw new Error(`Test took too long (${elapsedTimeInMs}ms > ${testTimeoutInMs}ms)!`);
- }
-});
+initializeTestTimeout(process.env.CI ? 5000 : 500);
// fail tests for unmocked requests
beforeEach(done => {
@@ -31,4 +29,34 @@ beforeEach(done => {
done();
});
+Vue.config.devtools = false;
+Vue.config.productionTip = false;
+
Vue.use(Translate);
+
+// workaround for JSDOM not supporting innerText
+// see https://github.com/jsdom/jsdom/issues/1245
+Object.defineProperty(global.Element.prototype, 'innerText', {
+ get() {
+ return this.textContent;
+ },
+ configurable: true, // make it so that it doesn't blow chunks on re-running tests with things like --watch
+});
+
+// convenience wrapper for migration from Karma
+Object.assign(global, {
+ loadFixtures: loadHTMLFixture,
+ setFixtures: setHTMLFixture,
+
+ // The following functions fill the fixtures cache in Karma.
+ // This is not necessary in Jest because we make no Ajax request.
+ loadJSONFixtures() {},
+ preloadFixtures() {},
+});
+
+// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
+Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
+ expect.extend({
+ [matcherName]: matcherFactory().compare,
+ });
+});
diff --git a/spec/javascripts/u2f/util_spec.js b/spec/frontend/u2f/util_spec.js
index 32cd6891384..32cd6891384 100644
--- a/spec/javascripts/u2f/util_spec.js
+++ b/spec/frontend/u2f/util_spec.js
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js
index 16c8c939a6f..16c8c939a6f 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_container_spec.js
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
index f7c2376eebf..f7c2376eebf 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_icon_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_icon_spec.js
diff --git a/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
index 994d6255324..994d6255324 100644
--- a/spec/javascripts/vue_mr_widget/components/states/commit_edit_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/commit_edit_spec.js
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
index daf1cc2d98b..daf1cc2d98b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
diff --git a/spec/javascripts/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 5cf6408cf34..9ee2f88c78d 100644
--- a/spec/javascripts/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
@@ -15,6 +15,7 @@ describe('Commits header component', () => {
isSquashEnabled: false,
targetBranch: 'master',
commitsCount: 5,
+ isFastForwardEnabled: false,
...props,
},
});
@@ -31,6 +32,27 @@ describe('Commits header component', () => {
const findTargetBranchMessage = () => wrapper.find('.label-branch');
const findModifyButton = () => wrapper.find('.modify-message-button');
+ describe('when fast-forward is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ isFastForwardEnabled: true,
+ isSquashEnabled: true,
+ });
+ });
+
+ it('has commits count message showing 1 commit', () => {
+ expect(findCommitsCountMessage().text()).toBe('1 commit');
+ });
+
+ it('has button with modify commit message', () => {
+ expect(findModifyButton().text()).toBe('Modify commit message');
+ });
+
+ it('does not have merge commit part of the message', () => {
+ expect(findHeaderWrapper().text()).not.toContain('1 merge commit');
+ });
+ });
+
describe('when collapsed', () => {
it('toggle has aria-label equal to Expand', () => {
createComponent();
@@ -78,6 +100,10 @@ describe('Commits header component', () => {
expect(findTargetBranchMessage().text()).toBe('master');
});
+
+ it('does has merge commit part of the message', () => {
+ expect(findHeaderWrapper().text()).toContain('1 merge commit');
+ });
});
describe('when expanded', () => {
diff --git a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index b356ea85cad..0f5d47b3bfe 100644
--- a/spec/javascripts/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -4,7 +4,7 @@ describe('getStateKey', () => {
it('should return proper state name', () => {
const context = {
mergeStatus: 'checked',
- mergeWhenPipelineSucceeds: false,
+ autoMergeEnabled: false,
canMerge: true,
onlyAllowMergeIfPipelineSucceeds: false,
isPipelineFailed: false,
@@ -31,9 +31,9 @@ describe('getStateKey', () => {
expect(bound()).toEqual('notAllowedToMerge');
- context.mergeWhenPipelineSucceeds = true;
+ context.autoMergeEnabled = true;
- expect(bound()).toEqual('mergeWhenPipelineSucceeds');
+ expect(bound()).toEqual('autoMergeEnabled');
context.isSHAMismatch = true;
@@ -80,7 +80,7 @@ describe('getStateKey', () => {
it('returns rebased state key', () => {
const context = {
mergeStatus: 'checked',
- mergeWhenPipelineSucceeds: false,
+ autoMergeEnabled: false,
canMerge: true,
onlyAllowMergeIfPipelineSucceeds: true,
isPipelineFailed: true,
diff --git a/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap
new file mode 100644
index 00000000000..add0c36a120
--- /dev/null
+++ b/spec/frontend/vue_shared/components/__snapshots__/resizable_chart_container_spec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Resizable Chart Container renders the component 1`] = `
+<div>
+ <div
+ class="slot"
+ >
+ <span
+ class="width"
+ >
+ 0
+ </span>
+
+ <span
+ class="height"
+ >
+ 0
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/javascripts/vue_shared/components/callout_spec.js b/spec/frontend/vue_shared/components/callout_spec.js
index 91208dfb31a..91208dfb31a 100644
--- a/spec/javascripts/vue_shared/components/callout_spec.js
+++ b/spec/frontend/vue_shared/components/callout_spec.js
diff --git a/spec/javascripts/vue_shared/components/code_block_spec.js b/spec/frontend/vue_shared/components/code_block_spec.js
index 6b91a20ff76..6b91a20ff76 100644
--- a/spec/javascripts/vue_shared/components/code_block_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_spec.js
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
index c4358f0d9cb..c4358f0d9cb 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
+++ b/spec/frontend/vue_shared/components/diff_viewer/viewers/mode_changed_spec.js
diff --git a/spec/javascripts/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js
index 0b3dbb61c96..0b3dbb61c96 100644
--- a/spec/javascripts/vue_shared/components/identicon_spec.js
+++ b/spec/frontend/vue_shared/components/identicon_spec.js
diff --git a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 9eac75fac96..d1de98f4a15 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -2,8 +2,8 @@ import Vue from 'vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockAssigneesList } from 'spec/boards/mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockAssigneesList } from '../../../../javascripts/boards/mock_data';
const createComponent = (assignees = mockAssigneesList, cssClass = '') => {
const Component = Vue.extend(IssueAssignees);
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
new file mode 100644
index 00000000000..2e93ec412b9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -0,0 +1,172 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+
+import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
+
+import { mockMilestone } from '../../../../javascripts/boards/mock_data';
+
+const createComponent = (milestone = mockMilestone) => {
+ const Component = Vue.extend(IssueMilestone);
+
+ return mount(Component, {
+ propsData: {
+ milestone,
+ },
+ sync: false,
+ });
+};
+
+describe('IssueMilestoneComponent', () => {
+ let wrapper;
+ let vm;
+
+ beforeEach(done => {
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
+
+ Vue.nextTick(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ describe('isMilestoneStarted', () => {
+ it('should return `false` when milestoneStart prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(false);
+ });
+
+ it('should return `true` when milestone start date is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestoneStarted).toBe(true);
+ });
+ });
+
+ describe('isMilestonePastDue', () => {
+ it('should return `false` when milestoneDue prop is not defined', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(false);
+ });
+
+ it('should return `true` when milestone due is past current date', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '1990-07-22',
+ }),
+ });
+
+ expect(wrapper.vm.isMilestonePastDue).toBe(true);
+ });
+ });
+
+ describe('milestoneDatesAbsolute', () => {
+ it('returns string containing absolute milestone due date', () => {
+ expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
+ });
+
+ it('returns string containing absolute milestone start date when due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('(January 1, 2018)');
+ });
+
+ it('returns empty string when both milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesAbsolute).toBe('');
+ });
+ });
+
+ describe('milestoneDatesHuman', () => {
+ it('returns string containing milestone due date when date is yet to be due', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ due_date: `${new Date().getFullYear() + 10}-01-01`,
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('years remaining');
+ });
+
+ it('returns string containing milestone start date when date has already started and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '1990-07-22',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Started');
+ });
+
+ it('returns string containing milestone start date when date is yet to start and due date is not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: `${new Date().getFullYear() + 10}-01-01`,
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toContain('Starts');
+ });
+
+ it('returns empty string when milestone start and due dates are not present', () => {
+ wrapper.setProps({
+ milestone: Object.assign({}, mockMilestone, {
+ start_date: '',
+ due_date: '',
+ }),
+ });
+
+ expect(wrapper.vm.milestoneDatesHuman).toBe('');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component root element with class `issue-milestone-details`', () => {
+ expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
+ });
+
+ it('renders milestone icon', () => {
+ expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
+ });
+
+ it('renders milestone title', () => {
+ expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
+ });
+
+ it('renders milestone tooltip', () => {
+ expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
+ mockMilestone.title,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
index aa7d6ea2e34..63880b85625 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_warning_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const IssueWarning = Vue.extend(issueWarning);
@@ -15,27 +15,37 @@ function formatWarning(string) {
describe('Issue Warning Component', () => {
describe('isLocked', () => {
it('should render locked issue warning information', () => {
- const vm = mountComponent(IssueWarning, {
+ const props = {
isLocked: true,
- });
+ lockedIssueDocsPath: 'docs/issues/locked',
+ };
+ const vm = mountComponent(IssueWarning, props);
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/lock$/);
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/lock$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
- 'This issue is locked. Only project members can comment.',
+ 'This issue is locked. Only project members can comment. Learn more',
);
+ expect(vm.$el.querySelector('a').href).toContain(props.lockedIssueDocsPath);
});
});
describe('isConfidential', () => {
it('should render confidential issue warning information', () => {
- const vm = mountComponent(IssueWarning, {
+ const props = {
isConfidential: true,
- });
+ confidentialIssueDocsPath: '/docs/issues/confidential',
+ };
+ const vm = mountComponent(IssueWarning, props);
- expect(vm.$el.querySelector('.icon use').href.baseVal).toMatch(/eye-slash$/);
+ expect(
+ vm.$el.querySelector('.icon use').getAttributeNS('http://www.w3.org/1999/xlink', 'href'),
+ ).toMatch(/eye-slash$/);
expect(formatWarning(vm.$el.querySelector('span').textContent)).toEqual(
- 'This is a confidential issue. Your comment will not be visible to the public.',
+ 'This is a confidential issue. People without permission will never get a notification. Learn more',
);
+ expect(vm.$el.querySelector('a').href).toContain(props.confidentialIssueDocsPath);
});
});
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
new file mode 100644
index 00000000000..e43d5301a50
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -0,0 +1,198 @@
+import Vue from 'vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { mount, createLocalVue } from '@vue/test-utils';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import {
+ defaultAssignees,
+ defaultMilestone,
+} from '../../../../javascripts/vue_shared/components/issue/related_issuable_mock_data';
+
+describe('RelatedIssuableItem', () => {
+ let wrapper;
+ const props = {
+ idKey: 1,
+ displayReference: 'gitlab-org/gitlab-test#1',
+ pathIdSeparator: '#',
+ path: `${gl.TEST_HOST}/path`,
+ title: 'title',
+ confidential: true,
+ dueDate: '1990-12-31',
+ weight: 10,
+ createdAt: '2018-12-01T00:00:00.00Z',
+ milestone: defaultMilestone,
+ assignees: defaultAssignees,
+ eventNamespace: 'relatedIssue',
+ };
+ const slots = {
+ dueDate: '<div class="js-due-date-slot"></div>',
+ weight: '<div class="js-weight-slot"></div>',
+ };
+
+ beforeEach(() => {
+ const localVue = createLocalVue();
+
+ wrapper = mount(localVue.extend(RelatedIssuableItem), {
+ localVue,
+ slots,
+ sync: false,
+ propsData: props,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains issuable-info-container class when canReorder is false', () => {
+ expect(wrapper.props('canReorder')).toBe(false);
+ expect(wrapper.find('.issuable-info-container').exists()).toBe(true);
+ });
+
+ it('does not render token state', () => {
+ expect(wrapper.find('.text-secondary svg').exists()).toBe(false);
+ });
+
+ it('does not render remove button', () => {
+ expect(wrapper.find({ ref: 'removeButton' }).exists()).toBe(false);
+ });
+
+ describe('token title', () => {
+ it('links to computedPath', () => {
+ expect(wrapper.find('.item-title a').attributes('href')).toEqual(wrapper.props('path'));
+ });
+
+ it('renders confidential icon', () => {
+ expect(wrapper.find('.confidential-icon').exists()).toBe(true);
+ });
+
+ it('renders title', () => {
+ expect(wrapper.find('.item-title a').text()).toEqual(props.title);
+ });
+ });
+
+ describe('token state', () => {
+ let tokenState;
+
+ beforeEach(done => {
+ wrapper.setProps({ state: 'opened' });
+
+ Vue.nextTick(() => {
+ tokenState = wrapper.find('.issue-token-state-icon-open');
+
+ done();
+ });
+ });
+
+ it('renders if hasState', () => {
+ expect(tokenState.exists()).toBe(true);
+ });
+
+ it('renders state title', () => {
+ const stateTitle = tokenState.attributes('data-original-title');
+ const formatedCreateDate = formatDate(props.createdAt);
+
+ expect(stateTitle).toContain('<span class="bold">Opened</span>');
+
+ expect(stateTitle).toContain(`<span class="text-tertiary">${formatedCreateDate}</span>`);
+ });
+
+ it('renders aria label', () => {
+ expect(tokenState.attributes('aria-label')).toEqual('opened');
+ });
+
+ it('renders open icon when open state', () => {
+ expect(tokenState.classes('issue-token-state-icon-open')).toBe(true);
+ });
+
+ it('renders close icon when close state', done => {
+ wrapper.setProps({
+ state: 'closed',
+ closedAt: '2018-12-01T00:00:00.00Z',
+ });
+
+ Vue.nextTick(() => {
+ expect(tokenState.classes('issue-token-state-icon-closed')).toBe(true);
+
+ done();
+ });
+ });
+ });
+
+ describe('token metadata', () => {
+ let tokenMetadata;
+
+ beforeEach(done => {
+ Vue.nextTick(() => {
+ tokenMetadata = wrapper.find('.item-meta');
+
+ done();
+ });
+ });
+
+ it('renders item path and ID', () => {
+ const pathAndID = tokenMetadata.find('.item-path-id').text();
+
+ expect(pathAndID).toContain('gitlab-org/gitlab-test');
+ expect(pathAndID).toContain('#1');
+ });
+
+ it('renders milestone icon and name', () => {
+ const milestoneIcon = tokenMetadata.find('.item-milestone svg use');
+ const milestoneTitle = tokenMetadata.find('.item-milestone .milestone-title');
+
+ expect(milestoneIcon.attributes('href')).toContain('clock');
+ expect(milestoneTitle.text()).toContain('Milestone title');
+ });
+
+ it('renders due date component', () => {
+ expect(tokenMetadata.find('.js-due-date-slot').exists()).toBe(true);
+ });
+
+ it('renders weight component', () => {
+ expect(tokenMetadata.find('.js-weight-slot').exists()).toBe(true);
+ });
+ });
+
+ describe('token assignees', () => {
+ it('renders assignees avatars', () => {
+ expect(wrapper.findAll('.item-assignees .user-avatar-link').length).toBe(2);
+ expect(wrapper.find('.item-assignees .avatar-counter').text()).toContain('+2');
+ });
+ });
+
+ describe('remove button', () => {
+ let removeBtn;
+
+ beforeEach(done => {
+ wrapper.setProps({ canRemove: true });
+ Vue.nextTick(() => {
+ removeBtn = wrapper.find({ ref: 'removeButton' });
+
+ done();
+ });
+ });
+
+ it('renders if canRemove', () => {
+ expect(removeBtn.exists()).toBe(true);
+ });
+
+ it('renders disabled button when removeDisabled', done => {
+ wrapper.vm.removeDisabled = true;
+
+ Vue.nextTick(() => {
+ expect(removeBtn.attributes('disabled')).toEqual('disabled');
+
+ done();
+ });
+ });
+
+ it('triggers onRemoveRequest when clicked', () => {
+ removeBtn.trigger('click');
+
+ const { relatedIssueRemoveRequest } = wrapper.emitted();
+
+ expect(relatedIssueRemoveRequest.length).toBe(1);
+ expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js b/spec/frontend/vue_shared/components/lib/utils/dom_utils_spec.js
index 2388660b0c2..2388660b0c2 100644
--- a/spec/javascripts/vue_shared/components/lib/utils/dom_utils_spec.js
+++ b/spec/frontend/vue_shared/components/lib/utils/dom_utils_spec.js
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
new file mode 100644
index 00000000000..3b6f67457ad
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -0,0 +1,103 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
+
+const localVue = createLocalVue();
+
+const DEFAULT_PROPS = {
+ canApply: true,
+ isApplied: false,
+ helpPagePath: 'path_to_docs',
+};
+
+describe('Suggestion Diff component', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ wrapper = shallowMount(localVue.extend(SuggestionDiffHeader), {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ localVue,
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findApplyButton = () => wrapper.find('.qa-apply-btn');
+ const findHeader = () => wrapper.find('.qa-suggestion-diff-header');
+ const findHelpButton = () => wrapper.find('.js-help-btn');
+ const findLoading = () => wrapper.find(GlLoadingIcon);
+
+ it('renders a suggestion header', () => {
+ createComponent();
+
+ const header = findHeader();
+
+ expect(header.exists()).toBe(true);
+ expect(header.html().includes('Suggested change')).toBe(true);
+ });
+
+ it('renders a help button', () => {
+ createComponent();
+
+ expect(findHelpButton().exists()).toBe(true);
+ });
+
+ it('renders an apply button', () => {
+ createComponent();
+
+ const applyBtn = findApplyButton();
+
+ expect(applyBtn.exists()).toBe(true);
+ expect(applyBtn.html().includes('Apply suggestion')).toBe(true);
+ });
+
+ it('does not render an apply button if `canApply` is set to false', () => {
+ createComponent({ canApply: false });
+
+ expect(findApplyButton().exists()).toBe(false);
+ });
+
+ describe('when apply suggestion is clicked', () => {
+ beforeEach(done => {
+ createComponent();
+
+ findApplyButton().vm.$emit('click');
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('emits apply', () => {
+ expect(wrapper.emittedByOrder()).toEqual([{ name: 'apply', args: [expect.any(Function)] }]);
+ });
+
+ it('hides apply button', () => {
+ expect(findApplyButton().exists()).toBe(false);
+ });
+
+ it('shows loading', () => {
+ expect(findLoading().exists()).toBe(true);
+ expect(wrapper.text()).toContain('Applying suggestion');
+ });
+
+ it('when callback of apply is called, hides loading', done => {
+ const [callback] = wrapper.emitted().apply[0];
+
+ callback();
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(findApplyButton().exists()).toBe(true);
+ expect(findLoading().exists()).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
new file mode 100644
index 00000000000..c8deac1c086
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_row_spec.js
@@ -0,0 +1,98 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import SuggestionDiffRow from '~/vue_shared/components/markdown/suggestion_diff_row.vue';
+
+const oldLine = {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: null,
+ old_line: 5,
+ rich_text: '-oldtext',
+ text: '-oldtext',
+ type: 'old',
+};
+
+const newLine = {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: 6,
+ old_line: null,
+ rich_text: '-newtext',
+ text: '-newtext',
+ type: 'new',
+};
+
+describe('SuggestionDiffRow', () => {
+ let wrapper;
+
+ const factory = (options = {}) => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(SuggestionDiffRow, {
+ localVue,
+ ...options,
+ });
+ };
+
+ const findOldLineWrapper = () => wrapper.find('.old_line');
+ const findNewLineWrapper = () => wrapper.find('.new_line');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correctly', () => {
+ factory({
+ propsData: {
+ line: oldLine,
+ },
+ });
+
+ expect(wrapper.is('.line_holder')).toBe(true);
+ });
+
+ describe('when passed line has type old', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ line: oldLine,
+ },
+ });
+ });
+
+ it('has old class when line has type old', () => {
+ expect(wrapper.find('td').classes()).toContain('old');
+ });
+
+ it('has old line number rendered', () => {
+ expect(findOldLineWrapper().text()).toBe('5');
+ });
+
+ it('has no new line number rendered', () => {
+ expect(findNewLineWrapper().text()).toBe('');
+ });
+ });
+
+ describe('when passed line has type new', () => {
+ beforeEach(() => {
+ factory({
+ propsData: {
+ line: newLine,
+ },
+ });
+ });
+
+ it('has new class when line has type new', () => {
+ expect(wrapper.find('td').classes()).toContain('new');
+ });
+
+ it('has no old line number rendered', () => {
+ expect(findOldLineWrapper().text()).toBe('');
+ });
+
+ it('has no new line number rendered', () => {
+ expect(findNewLineWrapper().text()).toBe('6');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
new file mode 100644
index 00000000000..f1943861523
--- /dev/null
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import modalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+describe('modal copy button', () => {
+ const Component = Vue.extend(modalCopyButton);
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ wrapper = shallowMount(Component, {
+ propsData: {
+ text: 'copy me',
+ title: 'Copy this value into Clipboard!',
+ },
+ });
+ });
+
+ describe('clipboard', () => {
+ it('should fire a `success` event on click', () => {
+ document.execCommand = jest.fn(() => true);
+ window.getSelection = jest.fn(() => ({
+ toString: jest.fn(() => 'test'),
+ removeAllRanges: jest.fn(),
+ }));
+ wrapper.trigger('click');
+ expect(wrapper.emitted().success).not.toBeEmpty();
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ });
+ it("should propagate the clipboard error event if execCommand doesn't work", () => {
+ document.execCommand = jest.fn(() => false);
+ wrapper.trigger('click');
+ expect(wrapper.emitted().error).not.toBeEmpty();
+ expect(document.execCommand).toHaveBeenCalledWith('copy');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
index 45f131194ca..eafff7f681e 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_note_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import issuePlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import createStore from '~/notes/stores';
-import { userDataMock } from '../../../notes/mock_data';
+import { userDataMock } from '../../../../javascripts/notes/mock_data';
describe('issue placeholder system note component', () => {
let store;
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
index 6013e85811a..976e38c15ee 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('placeholder system note component', () => {
let PlaceholderSystemNote;
diff --git a/spec/javascripts/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index adcb1c858aa..dc66150ab8d 100644
--- a/spec/javascripts/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -1,6 +1,9 @@
import Vue from 'vue';
import issueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import createStore from '~/notes/stores';
+import initMRPopovers from '~/mr_popover/index';
+
+jest.mock('~/mr_popover/index', () => jest.fn());
describe('system note component', () => {
let vm;
@@ -56,4 +59,8 @@ describe('system note component', () => {
it('removes wrapping paragraph from note HTML', () => {
expect(vm.$el.querySelector('.system-note-message').innerHTML).toEqual('<span>closed</span>');
});
+
+ it('should initMRPopovers onMount', () => {
+ expect(initMRPopovers).toHaveBeenCalled();
+ });
});
diff --git a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
index c15635f2105..be6c58f0683 100644
--- a/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
+++ b/spec/frontend/vue_shared/components/notes/timeline_entry_item_spec.js
@@ -1,7 +1,7 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-describe(TimelineEntryItem.name, () => {
+describe(`TimelineEntryItem`, () => {
let wrapper;
const factory = (options = {}) => {
diff --git a/spec/javascripts/vue_shared/components/pagination_links_spec.js b/spec/frontend/vue_shared/components/pagination_links_spec.js
index d0cb3731050..d0cb3731050 100644
--- a/spec/javascripts/vue_shared/components/pagination_links_spec.js
+++ b/spec/frontend/vue_shared/components/pagination_links_spec.js
diff --git a/spec/frontend/vue_shared/components/resizable_chart_container_spec.js b/spec/frontend/vue_shared/components/resizable_chart_container_spec.js
new file mode 100644
index 00000000000..8f533e8ab24
--- /dev/null
+++ b/spec/frontend/vue_shared/components/resizable_chart_container_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import ResizableChartContainer from '~/vue_shared/components/resizable_chart/resizable_chart_container.vue';
+import $ from 'jquery';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ debounceByAnimationFrame(callback) {
+ return jest.spyOn({ callback }, 'callback');
+ },
+}));
+
+describe('Resizable Chart Container', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(ResizableChartContainer, {
+ attachToDocument: true,
+ scopedSlots: {
+ default: `
+ <div class="slot" slot-scope="{ width, height }">
+ <span class="width">{{width}}</span>
+ <span class="height">{{height}}</span>
+ </div>
+ `,
+ },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the component', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('updates the slot width and height props', () => {
+ const width = 1920;
+ const height = 1080;
+
+ // JSDOM mocks and sets clientWidth/clientHeight to 0 so we set manually
+ wrapper.vm.$refs.chartWrapper = { clientWidth: width, clientHeight: height };
+
+ $(document).trigger('content.resize');
+
+ return Vue.nextTick().then(() => {
+ const widthNode = wrapper.find('.slot > .width');
+ const heightNode = wrapper.find('.slot > .height');
+
+ expect(parseInt(widthNode.text(), 10)).toEqual(width);
+ expect(parseInt(heightNode.text(), 10)).toEqual(height);
+ });
+ });
+
+ it('calls onResize on manual resize', () => {
+ $(document).trigger('content.resize');
+ expect(wrapper.vm.debouncedResize).toHaveBeenCalled();
+ });
+
+ it('calls onResize on page resize', () => {
+ window.dispatchEvent(new Event('resize'));
+ expect(wrapper.vm.debouncedResize).toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
index 6bff1521695..691ebe43d6b 100644
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('collapsedCalendarIcon', () => {
let vm;
@@ -26,7 +26,7 @@ describe('collapsedCalendarIcon', () => {
});
it('should emit click event when container is clicked', () => {
- const click = jasmine.createSpy();
+ const click = jest.fn();
vm.$on('click', click);
vm.$el.click();
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
index c507a97d37e..062ebfa01c9 100644
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('collapsedGroupedDatePicker', () => {
let vm;
@@ -13,7 +13,7 @@ describe('collapsedGroupedDatePicker', () => {
describe('toggleCollapse events', () => {
beforeEach(done => {
- spyOn(vm, 'toggleSidebar');
+ jest.spyOn(vm, 'toggleSidebar').mockImplementation(() => {});
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
index 805ba7b9947..5e2bca6efc9 100644
--- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/date_picker_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('sidebarDatePicker', () => {
let vm;
@@ -13,7 +13,7 @@ describe('sidebarDatePicker', () => {
});
it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => {
- const toggleCollapse = jasmine.createSpy();
+ const toggleCollapse = jest.fn();
vm.$on('toggleCollapse', toggleCollapse);
vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click();
@@ -90,7 +90,7 @@ describe('sidebarDatePicker', () => {
});
it('should emit saveDate when remove button is clicked', () => {
- const saveDate = jasmine.createSpy();
+ const saveDate = jest.fn();
vm.$on('saveDate', saveDate);
vm.$el.querySelector('.value-content .btn-blank').click();
@@ -110,7 +110,7 @@ describe('sidebarDatePicker', () => {
});
it('should emit toggleCollapse when toggle sidebar is clicked', () => {
- const toggleCollapse = jasmine.createSpy();
+ const toggleCollapse = jest.fn();
vm.$on('toggleCollapse', toggleCollapse);
vm.$el.querySelector('.title .gutter-toggle').click();
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
index c44b04009ca..6aee616c324 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -3,25 +3,35 @@ import Vue from 'vue';
import LabelsSelect from '~/labels_select';
import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
+import { mount } from '@vue/test-utils';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = (config = mockConfig) => {
const Component = Vue.extend(baseComponent);
- return mountComponent(Component, config);
+ return mount(Component, {
+ propsData: config,
+ sync: false,
+ });
};
describe('BaseComponent', () => {
+ let wrapper;
let vm;
- beforeEach(() => {
- vm = createComponent();
+ beforeEach(done => {
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
+
+ Vue.nextTick(done);
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('computed', () => {
@@ -31,11 +41,9 @@ describe('BaseComponent', () => {
});
it('returns correct string when showCreate prop is `false`', () => {
- const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false });
- const vmNonEditable = createComponent(mockConfigNonEditable);
+ wrapper.setProps({ showCreate: false });
- expect(vmNonEditable.hiddenInputName).toBe('label_id[]');
- vmNonEditable.$destroy();
+ expect(vm.hiddenInputName).toBe('label_id[]');
});
});
@@ -45,11 +53,9 @@ describe('BaseComponent', () => {
});
it('return `Create group label` when `isProject` prop is false', () => {
- const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false });
- const vmGroup = createComponent(mockConfigGroup);
+ wrapper.setProps({ isProject: false });
- expect(vmGroup.createLabelTitle).toBe('Create group label');
- vmGroup.$destroy();
+ expect(vm.createLabelTitle).toBe('Create group label');
});
});
@@ -59,11 +65,9 @@ describe('BaseComponent', () => {
});
it('return `Manage group labels` when `isProject` prop is false', () => {
- const mockConfigGroup = Object.assign({}, mockConfig, { isProject: false });
- const vmGroup = createComponent(mockConfigGroup);
+ wrapper.setProps({ isProject: false });
- expect(vmGroup.manageLabelsTitle).toBe('Manage group labels');
- vmGroup.$destroy();
+ expect(vm.manageLabelsTitle).toBe('Manage group labels');
});
});
});
@@ -71,7 +75,7 @@ describe('BaseComponent', () => {
describe('methods', () => {
describe('handleClick', () => {
it('emits onLabelClick event with label and list of labels as params', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleClick(mockLabels[0]);
expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
@@ -80,7 +84,7 @@ describe('BaseComponent', () => {
describe('handleCollapsedValueClick', () => {
it('emits toggleCollapse event on component', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleCollapsedValueClick();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
@@ -89,7 +93,7 @@ describe('BaseComponent', () => {
describe('handleDropdownHidden', () => {
it('emits onDropdownClose event on component', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleDropdownHidden();
expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
index 5cf6afebd7e..bb33dc6ea0f 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -2,9 +2,11 @@ import Vue from 'vue';
import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const componentConfig = Object.assign({}, mockConfig, {
fieldName: 'label_id[]',
@@ -45,12 +47,21 @@ describe('DropdownButtonComponent', () => {
});
const vmMoreLabels = createComponent(mockMoreLabels);
- expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more');
+ expect(vmMoreLabels.dropdownToggleText).toBe(
+ `Foo Label +${mockMoreLabels.labels.length - 1} more`,
+ );
vmMoreLabels.$destroy();
});
it('returns first label name when `labels` prop has only one item present', () => {
- expect(vm.dropdownToggleText).toBe('Foo Label');
+ const singleLabel = Object.assign({}, componentConfig, {
+ labels: [mockLabels[0]],
+ });
+ const vmSingleLabel = createComponent(singleLabel);
+
+ expect(vmSingleLabel.dropdownToggleText).toBe(mockLabels[0].title);
+
+ vmSingleLabel.$destroy();
});
});
});
@@ -73,7 +84,7 @@ describe('DropdownButtonComponent', () => {
const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
expect(dropdownToggleTextEl).not.toBeNull();
- expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label');
+ expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label +1 more');
});
it('renders dropdown button icon', () => {
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
index b8f32f96332..1c25d42682c 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -2,9 +2,8 @@ import Vue from 'vue';
import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockSuggestedColors } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockSuggestedColors } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = headerTitle => {
const Component = Vue.extend(dropdownCreateLabelComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
index 3711e9dac8c..989901a0012 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -2,9 +2,8 @@ import Vue from 'vue';
import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockConfig } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = (
labelsWebUrl = mockConfig.labelsWebUrl,
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
index 115e21e4f9f..c36a82e1a35 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(dropdownHeaderComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
index c30e619e76b..2fffb2e495e 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(dropdownSearchInputComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
index 6c84d2e167c..1616e657c81 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
const createComponent = (canEdit = true) => {
const Component = Vue.extend(dropdownTitleComponent);
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
index 804b33422bd..517f2c01c46 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -2,9 +2,8 @@ import Vue from 'vue';
import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockLabels } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import { mockLabels } from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = (labels = mockLabels) => {
const Component = Vue.extend(dropdownValueCollapsedComponent);
@@ -27,17 +26,20 @@ describe('DropdownValueCollapsedComponent', () => {
describe('computed', () => {
describe('labelsList', () => {
- it('returns empty text when `labels` prop is empty array', () => {
+ it('returns default text when `labels` prop is empty array', () => {
const vmEmptyLabels = createComponent([]);
- expect(vmEmptyLabels.labelsList).toBe('');
+ expect(vmEmptyLabels.labelsList).toBe('Labels');
vmEmptyLabels.$destroy();
});
it('returns labels names separated by coma when `labels` prop has more than one item', () => {
- const vmMoreLabels = createComponent(mockLabels.concat(mockLabels));
+ const labels = mockLabels.concat(mockLabels);
+ const vmMoreLabels = createComponent(labels);
+
+ const expectedText = labels.map(label => label.title).join(', ');
- expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label');
+ expect(vmMoreLabels.labelsList).toBe(expectedText);
vmMoreLabels.$destroy();
});
@@ -49,14 +51,19 @@ describe('DropdownValueCollapsedComponent', () => {
const vmMoreLabels = createComponent(mockMoreLabels);
- expect(vmMoreLabels.labelsList).toBe(
- 'Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more',
- );
+ const expectedText = `${mockMoreLabels
+ .slice(0, 5)
+ .map(label => label.title)
+ .join(', ')}, and ${mockMoreLabels.length - 5} more`;
+
+ expect(vmMoreLabels.labelsList).toBe(expectedText);
vmMoreLabels.$destroy();
});
it('returns first label name when `labels` prop has only one item present', () => {
- expect(vm.labelsList).toBe('Foo Label');
+ const text = mockLabels.map(label => label.title).join(', ');
+
+ expect(vm.labelsList).toBe(text);
});
});
});
@@ -64,7 +71,7 @@ describe('DropdownValueCollapsedComponent', () => {
describe('methods', () => {
describe('handleClick', () => {
it('emits onValueClick event on component', () => {
- spyOn(vm, '$emit');
+ jest.spyOn(vm, '$emit').mockImplementation(() => {});
vm.handleClick();
expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index 3fff781594f..ec143fec5d9 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -1,10 +1,13 @@
import Vue from 'vue';
+import $ from 'jquery';
import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockConfig, mockLabels } from './mock_data';
+import mountComponent from 'helpers/vue_mount_component_helper';
+import {
+ mockConfig,
+ mockLabels,
+} from '../../../../../javascripts/vue_shared/components/sidebar/labels_select/mock_data';
const createComponent = (
labels = mockLabels,
@@ -15,6 +18,7 @@ const createComponent = (
return mountComponent(Component, {
labels,
labelFilterBasePath,
+ enableScopedLabels: true,
});
};
@@ -67,6 +71,26 @@ describe('DropdownValueComponent', () => {
expect(styleObj.backgroundColor).toBe(label.color);
});
});
+
+ describe('scopedLabelsDescription', () => {
+ it('returns html for tooltip', () => {
+ const html = vm.scopedLabelsDescription(mockLabels[1]);
+ const $el = $.parseHTML(html);
+
+ expect($el[0]).toHaveClass('scoped-label-tooltip-title');
+ expect($el[2].textContent).toEqual(mockLabels[1].description);
+ });
+ });
+
+ describe('showScopedLabels', () => {
+ it('returns true if the label is scoped label', () => {
+ expect(vm.showScopedLabels(mockLabels[1])).toBe(true);
+ });
+
+ it('returns false when label is a regular label', () => {
+ expect(vm.showScopedLabels(mockLabels[0])).toBe(false);
+ });
+ });
});
describe('template', () => {
@@ -91,15 +115,25 @@ describe('DropdownValueComponent', () => {
);
});
- it('renders label element with tooltip and styles based on label details', () => {
+ it('renders label element and styles based on label details', () => {
const labelEl = vm.$el.querySelector('a span.badge.color-label');
expect(labelEl).not.toBeNull();
- expect(labelEl.dataset.placement).toBe('bottom');
- expect(labelEl.dataset.container).toBe('body');
- expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description);
expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
expect(labelEl.innerText.trim()).toBe(mockLabels[0].title);
});
+
+ describe('label is of scoped-label type', () => {
+ it('renders a scoped-label-wrapper span to incorporate 2 anchors', () => {
+ expect(vm.$el.querySelector('span.scoped-label-wrapper')).not.toBeNull();
+ });
+
+ it('renders anchor tag containing question icon', () => {
+ const anchor = vm.$el.querySelector('.scoped-label-wrapper a.scoped-label');
+
+ expect(anchor).not.toBeNull();
+ expect(anchor.querySelector('i.fa-question-circle')).not.toBeNull();
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
index c911a129173..5cf25ca6f81 100644
--- a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
describe('toggleSidebar', () => {
let vm;
@@ -23,7 +23,7 @@ describe('toggleSidebar', () => {
});
it('should emit toggle event when button clicked', () => {
- const toggle = jasmine.createSpy();
+ const toggle = jest.fn();
vm.$on('toggle', toggle);
vm.$el.click();
diff --git a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
index 536bb57b946..536bb57b946 100644
--- a/spec/javascripts/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
diff --git a/spec/frontend/vue_shared/droplab_dropdown_button_spec.js b/spec/frontend/vue_shared/droplab_dropdown_button_spec.js
new file mode 100644
index 00000000000..22295721328
--- /dev/null
+++ b/spec/frontend/vue_shared/droplab_dropdown_button_spec.js
@@ -0,0 +1,136 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+
+import DroplabDropdownButton from '~/vue_shared/components/droplab_dropdown_button.vue';
+
+const mockActions = [
+ {
+ title: 'Foo',
+ description: 'Some foo action',
+ },
+ {
+ title: 'Bar',
+ description: 'Some bar action',
+ },
+];
+
+const createComponent = ({
+ size = '',
+ dropdownClass = '',
+ actions = mockActions,
+ defaultAction = 0,
+}) => {
+ const localVue = createLocalVue();
+
+ return mount(DroplabDropdownButton, {
+ localVue,
+ propsData: {
+ size,
+ dropdownClass,
+ actions,
+ defaultAction,
+ },
+ });
+};
+
+describe('DroplabDropdownButton', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent({});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('data', () => {
+ it('contains `selectedAction` representing value of `defaultAction` prop', () => {
+ expect(wrapper.vm.selectedAction).toBe(0);
+ });
+ });
+
+ describe('computed', () => {
+ describe('selectedActionTitle', () => {
+ it('returns string containing title of selected action', () => {
+ wrapper.setData({ selectedAction: 0 });
+
+ expect(wrapper.vm.selectedActionTitle).toBe(mockActions[0].title);
+
+ wrapper.setData({ selectedAction: 1 });
+
+ expect(wrapper.vm.selectedActionTitle).toBe(mockActions[1].title);
+ });
+ });
+
+ describe('buttonSizeClass', () => {
+ it('returns string containing button sizing class based on `size` prop', done => {
+ const wrapperWithSize = createComponent({
+ size: 'sm',
+ });
+
+ wrapperWithSize.vm.$nextTick(() => {
+ expect(wrapperWithSize.vm.buttonSizeClass).toBe('btn-sm');
+
+ done();
+ wrapperWithSize.destroy();
+ });
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handlePrimaryActionClick', () => {
+ it('emits `onActionClick` event on component with selectedAction object as param', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.setData({ selectedAction: 0 });
+ wrapper.vm.handlePrimaryActionClick();
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionClick', mockActions[0]);
+ });
+ });
+
+ describe('handleActionClick', () => {
+ it('emits `onActionSelect` event on component with selectedAction index as param', () => {
+ jest.spyOn(wrapper.vm, '$emit');
+
+ wrapper.vm.handleActionClick(1);
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('onActionSelect', 1);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders default action button', () => {
+ const defaultButton = wrapper.findAll('.btn').at(0);
+
+ expect(defaultButton.text()).toBe(mockActions[0].title);
+ });
+
+ it('renders dropdown button', () => {
+ const dropdownButton = wrapper.findAll('.dropdown-toggle').at(0);
+
+ expect(dropdownButton.isVisible()).toBe(true);
+ });
+
+ it('renders dropdown actions', () => {
+ const dropdownActions = wrapper.findAll('.dropdown-menu li button');
+
+ Array(dropdownActions.length)
+ .fill()
+ .forEach((_, index) => {
+ const actionContent = dropdownActions.at(index).find('.description');
+
+ expect(actionContent.find('strong').text()).toBe(mockActions[index].title);
+ expect(actionContent.find('p').text()).toBe(mockActions[index].description);
+ });
+ });
+
+ it('renders divider between dropdown actions', () => {
+ const dropdownDivider = wrapper.find('.dropdown-menu .divider');
+
+ expect(dropdownDivider.isVisible()).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vuex_shared/modules/modal/mutations_spec.js b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js
index d07f8ba1e65..eaaf196d1ec 100644
--- a/spec/javascripts/vuex_shared/modules/modal/mutations_spec.js
+++ b/spec/frontend/vuex_shared/modules/modal/mutations_spec.js
@@ -2,7 +2,7 @@ import mutations from '~/vuex_shared/modules/modal/mutations';
import * as types from '~/vuex_shared/modules/modal/mutation_types';
describe('Vuex ModalModule mutations', () => {
- describe(types.SHOW, () => {
+ describe(`${types.SHOW}`, () => {
it('sets isVisible to true', () => {
const state = {
isVisible: false,
@@ -16,7 +16,7 @@ describe('Vuex ModalModule mutations', () => {
});
});
- describe(types.HIDE, () => {
+ describe(`${types.HIDE}`, () => {
it('sets isVisible to false', () => {
const state = {
isVisible: true,
@@ -30,7 +30,7 @@ describe('Vuex ModalModule mutations', () => {
});
});
- describe(types.OPEN, () => {
+ describe(`${types.OPEN}`, () => {
it('sets data and sets isVisible to true', () => {
const data = { id: 7 };
const state = {
diff --git a/spec/graphql/features/authorization_spec.rb b/spec/graphql/features/authorization_spec.rb
new file mode 100644
index 00000000000..c427893f9cc
--- /dev/null
+++ b/spec/graphql/features/authorization_spec.rb
@@ -0,0 +1,335 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Gitlab::Graphql::Authorization' do
+ set(:user) { create(:user) }
+
+ let(:permission_single) { :foo }
+ let(:permission_collection) { [:foo, :bar] }
+ let(:test_object) { double(name: 'My name') }
+ let(:query_string) { '{ object() { name } }' }
+ let(:result) { execute_query(query_type)['data'] }
+
+ subject { result['object'] }
+
+ shared_examples 'authorization with a single permission' do
+ it 'returns the protected field when user has permission' do
+ permit(permission_single)
+
+ expect(subject).to eq('name' => test_object.name)
+ end
+
+ it 'returns nil when user is not authorized' do
+ expect(subject).to be_nil
+ end
+ end
+
+ shared_examples 'authorization with a collection of permissions' do
+ it 'returns the protected field when user has all permissions' do
+ permit(*permission_collection)
+
+ expect(subject).to eq('name' => test_object.name)
+ end
+
+ it 'returns nil when user only has one of the permissions' do
+ permit(permission_collection.first)
+
+ expect(subject).to be_nil
+ end
+
+ it 'returns nil when user only has none of the permissions' do
+ expect(subject).to be_nil
+ end
+ end
+
+ before do
+ # By default, disallow all permissions.
+ allow(Ability).to receive(:allowed?).and_return(false)
+ end
+
+ describe 'Field authorizations' do
+ let(:type) { type_factory }
+
+ describe 'with a single permission' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_single
+ end
+ end
+
+ include_examples 'authorization with a single permission'
+ end
+
+ describe 'with a collection of permissions' do
+ let(:query_type) do
+ permissions = permission_collection
+ query_factory do |qt|
+ qt.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object } do
+ authorize permissions
+ end
+ end
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
+ end
+
+ describe 'Field authorizations when field is a built in type' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
+ end
+ end
+
+ describe 'with a single permission' do
+ let(:type) do
+ type_factory do |type|
+ type.field :name, GraphQL::STRING_TYPE, null: true, authorize: permission_single
+ end
+ end
+
+ it 'returns the protected field when user has permission' do
+ permit(permission_single)
+
+ expect(subject).to eq('name' => test_object.name)
+ end
+
+ it 'returns nil when user is not authorized' do
+ expect(subject).to eq('name' => nil)
+ end
+ end
+
+ describe 'with a collection of permissions' do
+ let(:type) do
+ permissions = permission_collection
+ type_factory do |type|
+ type.field :name, GraphQL::STRING_TYPE, null: true do
+ authorize permissions
+ end
+ end
+ end
+
+ it 'returns the protected field when user has all permissions' do
+ permit(*permission_collection)
+
+ expect(subject).to eq('name' => test_object.name)
+ end
+
+ it 'returns nil when user only has one of the permissions' do
+ permit(permission_collection.first)
+
+ expect(subject).to eq('name' => nil)
+ end
+
+ it 'returns nil when user only has none of the permissions' do
+ expect(subject).to eq('name' => nil)
+ end
+ end
+ end
+
+ describe 'Type authorizations' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }
+ end
+ end
+
+ describe 'with a single permission' do
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_single
+ end
+ end
+
+ include_examples 'authorization with a single permission'
+ end
+
+ describe 'with a collection of permissions' do
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_collection
+ end
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
+ end
+
+ describe 'type and field authorizations together' do
+ let(:permission_1) { permission_collection.first }
+ let(:permission_2) { permission_collection.last }
+
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_1
+ end
+ end
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type, null: true, resolve: ->(obj, args, ctx) { test_object }, authorize: permission_2
+ end
+ end
+
+ include_examples 'authorization with a collection of permissions'
+ end
+
+ describe 'type authorizations when applied to a relay connection' do
+ let(:query_string) { '{ object() { edges { node { name } } } }' }
+ let(:second_test_object) { double(name: 'Second thing') }
+
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_single
+ end
+ end
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) { [test_object, second_test_object] }
+ end
+ end
+
+ subject { result.dig('object', 'edges') }
+
+ it 'returns only the elements visible to the user' do
+ permit(permission_single)
+
+ expect(subject.size).to eq 1
+ expect(subject.first['node']).to eq('name' => test_object.name)
+ end
+
+ it 'returns nil when user is not authorized' do
+ expect(subject).to be_empty
+ end
+
+ describe 'limiting connections with multiple objects' do
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, type.connection_type, null: true, resolve: ->(obj, args, ctx) do
+ [test_object, second_test_object]
+ end
+ end
+ end
+
+ let(:query_string) { '{ object(first: 1) { edges { node { name } } } }' }
+
+ it 'only checks permissions for the first object' do
+ expect(Ability).to receive(:allowed?).with(user, permission_single, test_object) { true }
+ expect(Ability).not_to receive(:allowed?).with(user, permission_single, second_test_object)
+
+ expect(subject.size).to eq(1)
+ end
+ end
+ end
+
+ describe 'type authorizations when applied to a basic connection' do
+ let(:type) do
+ type_factory do |type|
+ type.authorize permission_single
+ end
+ end
+
+ let(:query_type) do
+ query_factory do |query|
+ query.field :object, [type], null: true, resolve: ->(obj, args, ctx) { [test_object] }
+ end
+ end
+
+ subject { result['object'].first }
+
+ include_examples 'authorization with a single permission'
+ end
+
+ describe 'Authorizations on active record relations' do
+ let!(:visible_project) { create(:project, :private) }
+ let!(:other_project) { create(:project, :private) }
+ let!(:visible_issues) { create_list(:issue, 2, project: visible_project) }
+ let!(:other_issues) { create_list(:issue, 2, project: other_project) }
+ let!(:user) { visible_project.owner }
+
+ let(:issue_type) do
+ type_factory do |type|
+ type.graphql_name 'FakeIssueType'
+ type.authorize :read_issue
+ type.field :id, GraphQL::ID_TYPE, null: false
+ end
+ end
+ let(:project_type) do |type|
+ type_factory do |type|
+ type.graphql_name 'FakeProjectType'
+ type.field :test_issues, issue_type.connection_type, null: false, resolve: -> (_, _, _) { Issue.where(project: [visible_project, other_project]) }
+ end
+ end
+ let(:query_type) do
+ query_factory do |query|
+ query.field :test_project, project_type, null: false, resolve: -> (_, _, _) { visible_project }
+ end
+ end
+ let(:query_string) do
+ <<~QRY
+ { testProject { testIssues(first: 3) { edges { node { id } } } } }
+ QRY
+ end
+
+ before do
+ allow(Ability).to receive(:allowed?).and_call_original
+ end
+
+ it 'renders the issues the user has access to' do
+ issue_edges = result['testProject']['testIssues']['edges']
+ issue_ids = issue_edges.map { |issue_edge| issue_edge['node']&.fetch('id') }
+
+ expect(issue_edges.size).to eq(visible_issues.size)
+ expect(issue_ids).to eq(visible_issues.map { |i| i.to_global_id.to_s })
+ end
+
+ it 'does not check access on fields that will not be rendered' do
+ expect(Ability).not_to receive(:allowed?).with(user, :read_issue, other_issues.last)
+
+ result
+ end
+ end
+
+ private
+
+ def permit(*permissions)
+ permissions.each do |permission|
+ allow(Ability).to receive(:allowed?).with(user, permission, test_object).and_return(true)
+ end
+ end
+
+ def type_factory
+ Class.new(Types::BaseObject) do
+ graphql_name 'TestType'
+
+ field :name, GraphQL::STRING_TYPE, null: true
+
+ yield(self) if block_given?
+ end
+ end
+
+ def query_factory
+ Class.new(Types::BaseObject) do
+ graphql_name 'TestQuery'
+
+ yield(self) if block_given?
+ end
+ end
+
+ def execute_query(query_type)
+ schema = Class.new(GraphQL::Schema) do
+ use Gitlab::Graphql::Authorize
+ use Gitlab::Graphql::Connections
+
+ query(query_type)
+ end
+
+ schema.execute(
+ query_string,
+ context: { current_user: user },
+ variables: {}
+ )
+ end
+end
diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb
index b9ddb427e85..4076c1f824b 100644
--- a/spec/graphql/gitlab_schema_spec.rb
+++ b/spec/graphql/gitlab_schema_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabSchema do
+ let(:user) { build :user }
+
it 'uses batch loading' do
expect(field_instrumenters).to include(BatchLoader::GraphQL)
end
@@ -31,7 +35,137 @@ describe GitlabSchema do
expect(connection).to eq(Gitlab::Graphql::Connections::KeysetConnection)
end
+ describe '.execute' do
+ context 'for different types of users' do
+ context 'when no context' do
+ it 'returns DEFAULT_MAX_COMPLEXITY' do
+ expect(GraphQL::Schema)
+ .to receive(:execute)
+ .with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY))
+
+ described_class.execute('query')
+ end
+ end
+
+ context 'when no user' do
+ it 'returns DEFAULT_MAX_COMPLEXITY' do
+ expect(GraphQL::Schema)
+ .to receive(:execute)
+ .with('query', hash_including(max_complexity: GitlabSchema::DEFAULT_MAX_COMPLEXITY))
+
+ described_class.execute('query', context: {})
+ end
+
+ it 'returns DEFAULT_MAX_DEPTH' do
+ expect(GraphQL::Schema)
+ .to receive(:execute)
+ .with('query', hash_including(max_depth: GitlabSchema::DEFAULT_MAX_DEPTH))
+
+ described_class.execute('query', context: {})
+ end
+ end
+
+ context 'when a logged in user' do
+ it 'returns AUTHENTICATED_COMPLEXITY' do
+ expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::AUTHENTICATED_COMPLEXITY))
+
+ described_class.execute('query', context: { current_user: user })
+ end
+
+ it 'returns AUTHENTICATED_MAX_DEPTH' do
+ expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_depth: GitlabSchema::AUTHENTICATED_MAX_DEPTH))
+
+ described_class.execute('query', context: { current_user: user })
+ end
+ end
+
+ context 'when an admin user' do
+ it 'returns ADMIN_COMPLEXITY' do
+ user = build :user, :admin
+
+ expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: GitlabSchema::ADMIN_COMPLEXITY))
+
+ described_class.execute('query', context: { current_user: user })
+ end
+ end
+
+ context 'when max_complexity passed on the query' do
+ it 'returns what was passed on the query' do
+ expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_complexity: 1234))
+
+ described_class.execute('query', max_complexity: 1234)
+ end
+ end
+
+ context 'when max_depth passed on the query' do
+ it 'returns what was passed on the query' do
+ expect(GraphQL::Schema).to receive(:execute).with('query', hash_including(max_depth: 1234))
+
+ described_class.execute('query', max_depth: 1234)
+ end
+ end
+ end
+ end
+
+ describe '.id_from_object' do
+ it 'returns a global id' do
+ expect(described_class.id_from_object(build(:project, id: 1))).to be_a(GlobalID)
+ end
+
+ it "raises a meaningful error if a global id couldn't be generated" do
+ expect { described_class.id_from_object(build(:commit)) }
+ .to raise_error(RuntimeError, /include `GlobalID::Identification` into/i)
+ end
+ end
+
+ describe '.object_from_id' do
+ context 'for subclasses of `ApplicationRecord`' do
+ it 'returns the correct record' do
+ user = create(:user)
+
+ result = described_class.object_from_id(user.to_global_id.to_s)
+
+ expect(result.__sync).to eq(user)
+ end
+
+ it 'batchloads the queries' do
+ user1 = create(:user)
+ user2 = create(:user)
+
+ expect do
+ [described_class.object_from_id(user1.to_global_id),
+ described_class.object_from_id(user2.to_global_id)].map(&:__sync)
+ end.not_to exceed_query_limit(1)
+ end
+ end
+
+ context 'for other classes' do
+ # We cannot use an anonymous class here as `GlobalID` expects `.name` not
+ # to return `nil`
+ class TestGlobalId
+ include GlobalID::Identification
+ attr_accessor :id
+
+ def initialize(id)
+ @id = id
+ end
+ end
+
+ it 'falls back to a regular find' do
+ result = TestGlobalId.new(123)
+
+ expect(TestGlobalId).to receive(:find).with("123").and_return(result)
+
+ expect(described_class.object_from_id(result.to_global_id)).to eq(result)
+ end
+ end
+
+ it 'raises the correct error on invalid input' do
+ expect { described_class.object_from_id("bogus id") }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+
def field_instrumenters
- described_class.instrumenters[:field]
+ described_class.instrumenters[:field] + described_class.instrumenters[:field_after_built_ins]
end
end
diff --git a/spec/graphql/resolvers/base_resolver_spec.rb b/spec/graphql/resolvers/base_resolver_spec.rb
index e3a34762b62..c162fdbbb47 100644
--- a/spec/graphql/resolvers/base_resolver_spec.rb
+++ b/spec/graphql/resolvers/base_resolver_spec.rb
@@ -28,4 +28,21 @@ describe Resolvers::BaseResolver do
expect(result).to eq(test: 1)
end
end
+
+ context 'when field is a connection' do
+ it 'increases complexity based on arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1)
+
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 3
+ expect(field.to_graphql.complexity.call({}, { search: 'foo' }, 1)).to eq 7
+ end
+
+ it 'does not increase complexity when filtering by iids' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100)
+
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo' }, 1)).to eq 6
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo', iid: 1 }, 1)).to eq 3
+ expect(field.to_graphql.complexity.call({}, { sort: 'foo', iids: [1, 2, 3] }, 1)).to eq 3
+ end
+ end
end
diff --git a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
index ea7159eacf9..3140af27af5 100644
--- a/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
+++ b/spec/graphql/resolvers/concerns/resolves_pipelines_spec.rb
@@ -46,6 +46,14 @@ describe ResolvesPipelines do
expect(resolve_pipelines({}, {})).to be_empty
end
+ it 'increases field complexity based on arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, null: false, max_page_size: 1)
+
+ expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 2
+ expect(field.to_graphql.complexity.call({}, { sha: 'foo' }, 1)).to eq 4
+ expect(field.to_graphql.complexity.call({}, { sha: 'ref' }, 1)).to eq 4
+ end
+
def resolve_pipelines(args = {}, context = { current_user: current_user })
resolve(resolver, obj: project, args: args, ctx: context)
end
diff --git a/spec/graphql/resolvers/group_resolver_spec.rb b/spec/graphql/resolvers/group_resolver_spec.rb
new file mode 100644
index 00000000000..5eb9cd06d15
--- /dev/null
+++ b/spec/graphql/resolvers/group_resolver_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::GroupResolver do
+ include GraphqlHelpers
+
+ set(:group1) { create(:group) }
+ set(:group2) { create(:group) }
+
+ describe '#resolve' do
+ it 'batch-resolves groups by full path' do
+ paths = [group1.full_path, group2.full_path]
+
+ result = batch(max_queries: 1) do
+ paths.map { |path| resolve_group(path) }
+ end
+
+ expect(result).to contain_exactly(group1, group2)
+ end
+
+ it 'resolves an unknown full_path to nil' do
+ result = batch { resolve_group('unknown/project') }
+
+ expect(result).to be_nil
+ end
+ end
+
+ def resolve_group(full_path)
+ resolve(described_class, args: { full_path: full_path })
+ end
+end
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 5f9c180cbb7..798fe00de97 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -4,105 +4,127 @@ describe Resolvers::IssuesResolver do
include GraphqlHelpers
let(:current_user) { create(:user) }
- set(:project) { create(:project) }
- set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
- set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
- set(:label1) { create(:label, project: project) }
- set(:label2) { create(:label, project: project) }
-
- before do
- project.add_developer(current_user)
- create(:label_link, label: label1, target: issue1)
- create(:label_link, label: label1, target: issue2)
- create(:label_link, label: label2, target: issue2)
- end
- describe '#resolve' do
- it 'finds all issues' do
- expect(resolve_issues).to contain_exactly(issue1, issue2)
+ context "with a project" do
+ set(:project) { create(:project) }
+ set(:issue1) { create(:issue, project: project, state: :opened, created_at: 3.hours.ago, updated_at: 3.hours.ago) }
+ set(:issue2) { create(:issue, project: project, state: :closed, title: 'foo', created_at: 1.hour.ago, updated_at: 1.hour.ago, closed_at: 1.hour.ago) }
+ set(:label1) { create(:label, project: project) }
+ set(:label2) { create(:label, project: project) }
+
+ before do
+ project.add_developer(current_user)
+ create(:label_link, label: label1, target: issue1)
+ create(:label_link, label: label1, target: issue2)
+ create(:label_link, label: label2, target: issue2)
end
- it 'filters by state' do
- expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
- expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
- end
+ describe '#resolve' do
+ it 'finds all issues' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
- it 'filters by labels' do
- expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
- expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
- end
+ it 'filters by state' do
+ expect(resolve_issues(state: 'opened')).to contain_exactly(issue1)
+ expect(resolve_issues(state: 'closed')).to contain_exactly(issue2)
+ end
- describe 'filters by created_at' do
- it 'filters by created_before' do
- expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ it 'filters by labels' do
+ expect(resolve_issues(label_name: [label1.title])).to contain_exactly(issue1, issue2)
+ expect(resolve_issues(label_name: [label1.title, label2.title])).to contain_exactly(issue2)
end
- it 'filters by created_after' do
- expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ describe 'filters by created_at' do
+ it 'filters by created_before' do
+ expect(resolve_issues(created_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by created_after' do
+ expect(resolve_issues(created_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- end
- describe 'filters by updated_at' do
- it 'filters by updated_before' do
- expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ describe 'filters by updated_at' do
+ it 'filters by updated_before' do
+ expect(resolve_issues(updated_before: 2.hours.ago)).to contain_exactly(issue1)
+ end
+
+ it 'filters by updated_after' do
+ expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- it 'filters by updated_after' do
- expect(resolve_issues(updated_after: 2.hours.ago)).to contain_exactly(issue2)
+ describe 'filters by closed_at' do
+ let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
+
+ it 'filters by closed_before' do
+ expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ end
+
+ it 'filters by closed_after' do
+ expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ end
end
- end
- describe 'filters by closed_at' do
- let!(:issue3) { create(:issue, project: project, state: :closed, closed_at: 3.hours.ago) }
+ it 'searches issues' do
+ expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
+ end
- it 'filters by closed_before' do
- expect(resolve_issues(closed_before: 2.hours.ago)).to contain_exactly(issue3)
+ it 'sort issues' do
+ expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
end
- it 'filters by closed_after' do
- expect(resolve_issues(closed_after: 2.hours.ago)).to contain_exactly(issue2)
+ it 'returns issues user can see' do
+ project.add_guest(current_user)
+
+ create(:issue, confidential: true)
+
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
end
- end
- it 'searches issues' do
- expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
- end
+ it 'finds a specific issue with iid' do
+ expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
+ end
- it 'sort issues' do
- expect(resolve_issues(sort: 'created_desc')).to eq [issue2, issue1]
- end
+ it 'finds a specific issue with iids' do
+ expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
+ end
- it 'returns issues user can see' do
- project.add_guest(current_user)
+ it 'finds multiple issues with iids' do
+ expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
+ .to contain_exactly(issue1, issue2)
+ end
- create(:issue, confidential: true)
+ it 'finds only the issues within the project we are looking at' do
+ another_project = create(:project)
+ iids = [issue1, issue2].map(&:iid)
- expect(resolve_issues).to contain_exactly(issue1, issue2)
- end
+ iids.each do |iid|
+ create(:issue, project: another_project, iid: iid)
+ end
- it 'finds a specific issue with iid' do
- expect(resolve_issues(iid: issue1.iid)).to contain_exactly(issue1)
+ expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
+ end
end
+ end
- it 'finds a specific issue with iids' do
- expect(resolve_issues(iids: issue1.iid)).to contain_exactly(issue1)
+ context "when passing a non existent, batch loaded project" do
+ let(:project) do
+ BatchLoader.for("non-existent-path").batch do |_fake_paths, loader, _|
+ loader.call("non-existent-path", nil)
+ end
end
- it 'finds multiple issues with iids' do
- expect(resolve_issues(iids: [issue1.iid, issue2.iid]))
- .to contain_exactly(issue1, issue2)
+ it "returns nil without breaking" do
+ expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end
+ end
- it 'finds only the issues within the project we are looking at' do
- another_project = create(:project)
- iids = [issue1, issue2].map(&:iid)
-
- iids.each do |iid|
- create(:issue, project: another_project, iid: iid)
- end
+ it 'increases field complexity based on arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100)
- expect(resolve_issues(iids: iids)).to contain_exactly(issue1, issue2)
- end
+ expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 4
+ expect(field.to_graphql.complexity.call({}, { labelName: 'foo' }, 1)).to eq 8
end
def resolve_issues(args = {}, context = { current_user: current_user })
diff --git a/spec/graphql/resolvers/metadata_resolver_spec.rb b/spec/graphql/resolvers/metadata_resolver_spec.rb
new file mode 100644
index 00000000000..e662ed127a5
--- /dev/null
+++ b/spec/graphql/resolvers/metadata_resolver_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Resolvers::MetadataResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ it 'returns version and revision' do
+ expect(resolve(described_class)).to eq(version: Gitlab::VERSION, revision: Gitlab.revision)
+ end
+ end
+end
diff --git a/spec/graphql/resolvers/namespace_projects_resolver_spec.rb b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
new file mode 100644
index 00000000000..20e197e9f73
--- /dev/null
+++ b/spec/graphql/resolvers/namespace_projects_resolver_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::NamespaceProjectsResolver, :nested_groups do
+ include GraphqlHelpers
+
+ let(:current_user) { create(:user) }
+
+ context "with a group" do
+ let(:group) { create(:group) }
+ let(:namespace) { group }
+ let(:project1) { create(:project, namespace: namespace) }
+ let(:project2) { create(:project, namespace: namespace) }
+ let(:nested_group) { create(:group, parent: group) }
+ let(:nested_project) { create(:project, group: nested_group) }
+
+ before do
+ project1.add_developer(current_user)
+ project2.add_developer(current_user)
+ nested_project.add_developer(current_user)
+ end
+
+ describe '#resolve' do
+ it 'finds all projects' do
+ expect(resolve_projects).to contain_exactly(project1, project2)
+ end
+
+ it 'finds all projects including the subgroups' do
+ expect(resolve_projects(include_subgroups: true)).to contain_exactly(project1, project2, nested_project)
+ end
+
+ context 'with an user namespace' do
+ let(:namespace) { current_user.namespace }
+
+ it 'finds all projects' do
+ expect(resolve_projects).to contain_exactly(project1, project2)
+ end
+
+ it 'finds all projects including the subgroups' do
+ expect(resolve_projects(include_subgroups: true)).to contain_exactly(project1, project2)
+ end
+ end
+ end
+ end
+
+ context "when passing a non existent, batch loaded namespace" do
+ let(:namespace) do
+ BatchLoader.for("non-existent-path").batch do |_fake_paths, loader, _|
+ loader.call("non-existent-path", nil)
+ end
+ end
+
+ it "returns nil without breaking" do
+ expect(resolve_projects).to be_empty
+ end
+ end
+
+ it 'has an high complexity regardless of arguments' do
+ field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 100)
+
+ expect(field.to_graphql.complexity.call({}, {}, 1)).to eq 24
+ expect(field.to_graphql.complexity.call({}, { include_subgroups: true }, 1)).to eq 24
+ end
+
+ def resolve_projects(args = { include_subgroups: false }, context = { current_user: current_user })
+ resolve(described_class, obj: namespace, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/resolvers/project_resolver_spec.rb b/spec/graphql/resolvers/project_resolver_spec.rb
index d4990c6492c..4fdbb3aa43e 100644
--- a/spec/graphql/resolvers/project_resolver_spec.rb
+++ b/spec/graphql/resolvers/project_resolver_spec.rb
@@ -26,6 +26,14 @@ describe Resolvers::ProjectResolver do
end
end
+ it 'does not increase complexity depending on number of load limits' do
+ field1 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 100)
+ field2 = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: described_class, null: false, max_page_size: 1)
+
+ expect(field1.to_graphql.complexity.call({}, {}, 1)).to eq 2
+ expect(field2.to_graphql.complexity.call({}, {}, 1)).to eq 2
+ end
+
def resolve_project(full_path)
resolve(described_class, args: { full_path: full_path })
end
diff --git a/spec/graphql/resolvers/tree_resolver_spec.rb b/spec/graphql/resolvers/tree_resolver_spec.rb
new file mode 100644
index 00000000000..9f95b740ab1
--- /dev/null
+++ b/spec/graphql/resolvers/tree_resolver_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Resolvers::TreeResolver do
+ include GraphqlHelpers
+
+ let(:repository) { create(:project, :repository).repository }
+
+ describe '#resolve' do
+ it 'resolves to a tree' do
+ result = resolve_repository({ ref: "master" })
+
+ expect(result).to be_an_instance_of(Tree)
+ end
+
+ it 'resolve to a recursive tree' do
+ result = resolve_repository({ ref: "master", recursive: true })
+
+ expect(result.trees[4].path).to eq('files/html')
+ end
+
+ context 'when repository does not exist' do
+ it 'returns nil' do
+ allow(repository).to receive(:exists?).and_return(false)
+
+ result = resolve_repository({ ref: "master" })
+
+ expect(result).to be(nil)
+ end
+ end
+ end
+
+ def resolve_repository(args)
+ resolve(described_class, obj: repository, args: args)
+ end
+end
diff --git a/spec/graphql/types/base_field_spec.rb b/spec/graphql/types/base_field_spec.rb
new file mode 100644
index 00000000000..0d3c3e37daf
--- /dev/null
+++ b/spec/graphql/types/base_field_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::BaseField do
+ context 'when considering complexity' do
+ let(:resolver) do
+ Class.new(described_class) do
+ def self.resolver_complexity(args, child_complexity:)
+ 2 if args[:foo]
+ end
+
+ def self.complexity_multiplier(args)
+ 0.01
+ end
+ end
+ end
+
+ it 'defaults to 1' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true)
+
+ expect(field.to_graphql.complexity).to eq 1
+ end
+
+ it 'has specified value' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, null: true, complexity: 12)
+
+ expect(field.to_graphql.complexity).to eq 12
+ end
+
+ context 'when field has a resolver proc' do
+ context 'and is a connection' do
+ let(:field) { described_class.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: resolver, max_page_size: 100, null: true) }
+
+ it 'sets complexity depending on arguments for resolvers' do
+ expect(field.to_graphql.complexity.call({}, {}, 2)).to eq 4
+ expect(field.to_graphql.complexity.call({}, { first: 50 }, 2)).to eq 3
+ end
+
+ it 'sets complexity depending on number load limits for resolvers' do
+ expect(field.to_graphql.complexity.call({}, { first: 1 }, 2)).to eq 2
+ expect(field.to_graphql.complexity.call({}, { first: 1, foo: true }, 2)).to eq 4
+ end
+ end
+
+ context 'and is not a connection' do
+ it 'sets complexity as normal' do
+ field = described_class.new(name: 'test', type: GraphQL::STRING_TYPE, resolver_class: resolver, max_page_size: 100, null: true)
+
+ expect(field.to_graphql.complexity.call({}, {}, 2)).to eq 2
+ expect(field.to_graphql.complexity.call({}, { first: 50 }, 2)).to eq 2
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/ci/detailed_status_type_spec.rb b/spec/graphql/types/ci/detailed_status_type_spec.rb
new file mode 100644
index 00000000000..a21162adb42
--- /dev/null
+++ b/spec/graphql/types/ci/detailed_status_type_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Types::Ci::DetailedStatusType do
+ it { expect(described_class.graphql_name).to eq('DetailedStatus') }
+
+ it "has all fields" do
+ expect(described_class).to have_graphql_fields(:group, :icon, :favicon,
+ :details_path, :has_details,
+ :label, :text, :tooltip)
+ end
+end
diff --git a/spec/graphql/types/group_type_spec.rb b/spec/graphql/types/group_type_spec.rb
new file mode 100644
index 00000000000..3dd5b602aa2
--- /dev/null
+++ b/spec/graphql/types/group_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Group'] do
+ it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Group) }
+
+ it { expect(described_class.graphql_name).to eq('Group') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_group) }
+end
diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb
index 63a07647a60..bae560829cc 100644
--- a/spec/graphql/types/issue_type_spec.rb
+++ b/spec/graphql/types/issue_type_spec.rb
@@ -4,4 +4,12 @@ describe GitlabSchema.types['Issue'] do
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Issue) }
it { expect(described_class.graphql_name).to eq('Issue') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_issue) }
+
+ it 'has specific fields' do
+ %i[relative_position web_path web_url reference].each do |field_name|
+ expect(described_class).to have_graphql_field(field_name)
+ end
+ end
end
diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb
index c369953e3ea..89c12879074 100644
--- a/spec/graphql/types/merge_request_type_spec.rb
+++ b/spec/graphql/types/merge_request_type_spec.rb
@@ -3,14 +3,9 @@ require 'spec_helper'
describe GitlabSchema.types['MergeRequest'] do
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::MergeRequest) }
- describe 'head pipeline' do
- it 'has a head pipeline field' do
- expect(described_class).to have_graphql_field(:head_pipeline)
- end
+ it { expect(described_class).to require_graphql_authorizations(:read_merge_request) }
- it 'authorizes the field' do
- expect(described_class.fields['headPipeline'])
- .to require_graphql_authorizations(:read_pipeline)
- end
+ describe 'nested head pipeline' do
+ it { expect(described_class).to have_graphql_field(:head_pipeline) }
end
end
diff --git a/spec/graphql/types/metadata_type_spec.rb b/spec/graphql/types/metadata_type_spec.rb
new file mode 100644
index 00000000000..55205bf5b6a
--- /dev/null
+++ b/spec/graphql/types/metadata_type_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe GitlabSchema.types['Metadata'] do
+ it { expect(described_class.graphql_name).to eq('Metadata') }
+end
diff --git a/spec/graphql/types/milestone_type_spec.rb b/spec/graphql/types/milestone_type_spec.rb
new file mode 100644
index 00000000000..f7ee79eae9f
--- /dev/null
+++ b/spec/graphql/types/milestone_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Milestone'] do
+ it { expect(described_class.graphql_name).to eq('Milestone') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_milestone) }
+end
diff --git a/spec/graphql/types/namespace_type_spec.rb b/spec/graphql/types/namespace_type_spec.rb
new file mode 100644
index 00000000000..b4144cc4121
--- /dev/null
+++ b/spec/graphql/types/namespace_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Namespace'] do
+ it { expect(described_class.graphql_name).to eq('Namespace') }
+
+ it { expect(described_class).to have_graphql_field(:projects) }
+end
diff --git a/spec/graphql/types/permission_types/issue_spec.rb b/spec/graphql/types/permission_types/issue_spec.rb
index c3f84629aa2..f0fbeda202f 100644
--- a/spec/graphql/types/permission_types/issue_spec.rb
+++ b/spec/graphql/types/permission_types/issue_spec.rb
@@ -7,6 +7,8 @@ describe Types::PermissionTypes::Issue do
:create_note, :reopen_issue
]
- expect(described_class).to have_graphql_fields(expected_permissions)
+ expected_permissions.each do |permission|
+ expect(described_class).to have_graphql_field(permission)
+ end
end
end
diff --git a/spec/graphql/types/permission_types/project_spec.rb b/spec/graphql/types/permission_types/project_spec.rb
index 4288412eda3..4974995b587 100644
--- a/spec/graphql/types/permission_types/project_spec.rb
+++ b/spec/graphql/types/permission_types/project_spec.rb
@@ -13,6 +13,8 @@ describe Types::PermissionTypes::Project do
:update_wiki, :destroy_wiki, :create_pages, :destroy_pages, :read_pages_content
]
- expect(described_class).to have_graphql_fields(expected_permissions)
+ expected_permissions.each do |permission|
+ expect(described_class).to have_graphql_field(permission)
+ end
end
end
diff --git a/spec/graphql/types/project_statistics_type_spec.rb b/spec/graphql/types/project_statistics_type_spec.rb
new file mode 100644
index 00000000000..e9feac57a36
--- /dev/null
+++ b/spec/graphql/types/project_statistics_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['ProjectStatistics'] do
+ it "has all the required fields" do
+ is_expected.to have_graphql_fields(:storage_size, :repository_size, :lfs_objects_size,
+ :build_artifacts_size, :packages_size, :commit_count,
+ :wiki_size)
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index e8f1c84f8d6..cb5ac2e3cb1 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -5,19 +5,11 @@ describe GitlabSchema.types['Project'] do
it { expect(described_class.graphql_name).to eq('Project') }
+ it { expect(described_class).to require_graphql_authorizations(:read_project) }
+
describe 'nested merge request' do
it { expect(described_class).to have_graphql_field(:merge_requests) }
it { expect(described_class).to have_graphql_field(:merge_request) }
-
- it 'authorizes the merge request' do
- expect(described_class.fields['mergeRequest'])
- .to require_graphql_authorizations(:read_merge_request)
- end
-
- it 'authorizes the merge requests' do
- expect(described_class.fields['mergeRequests'])
- .to require_graphql_authorizations(:read_merge_request)
- end
end
describe 'nested issues' do
@@ -25,4 +17,8 @@ describe GitlabSchema.types['Project'] do
end
it { is_expected.to have_graphql_field(:pipelines) }
+
+ it { is_expected.to have_graphql_field(:repository) }
+
+ it { is_expected.to have_graphql_field(:statistics) }
end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index e1df6f9811d..af1972a2513 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -5,7 +5,17 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
- it { is_expected.to have_graphql_fields(:project, :echo) }
+ it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata) }
+
+ describe 'namespace field' do
+ subject { described_class.fields['namespace'] }
+
+ it 'finds namespaces by full path' do
+ is_expected.to have_graphql_arguments(:full_path)
+ is_expected.to have_graphql_type(Types::NamespaceType)
+ is_expected.to have_graphql_resolver(Resolvers::NamespaceResolver)
+ end
+ end
describe 'project field' do
subject { described_class.fields['project'] }
@@ -15,9 +25,18 @@ describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_type(Types::ProjectType)
is_expected.to have_graphql_resolver(Resolvers::ProjectResolver)
end
+ end
+
+ describe 'metadata field' do
+ subject { described_class.fields['metadata'] }
+
+ it 'returns metadata' do
+ is_expected.to have_graphql_type(Types::MetadataType)
+ is_expected.to have_graphql_resolver(Resolvers::MetadataResolver)
+ end
- it 'authorizes with read_project' do
- is_expected.to require_graphql_authorizations(:read_project)
+ it 'authorizes with read_instance_metadata' do
+ is_expected.to require_graphql_authorizations(:read_instance_metadata)
end
end
end
diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb
new file mode 100644
index 00000000000..8a8238f2a2a
--- /dev/null
+++ b/spec/graphql/types/repository_type_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe GitlabSchema.types['Repository'] do
+ it { expect(described_class.graphql_name).to eq('Repository') }
+
+ it { expect(described_class).to require_graphql_authorizations(:download_code) }
+
+ it { is_expected.to have_graphql_field(:root_ref) }
+
+ it { is_expected.to have_graphql_field(:tree) }
+end
diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb
new file mode 100644
index 00000000000..b12e214ca84
--- /dev/null
+++ b/spec/graphql/types/tree/blob_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::Tree::BlobType do
+ it { expect(described_class.graphql_name).to eq('Blob') }
+
+ it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url) }
+end
diff --git a/spec/graphql/types/tree/submodule_type_spec.rb b/spec/graphql/types/tree/submodule_type_spec.rb
new file mode 100644
index 00000000000..bdb3149b41c
--- /dev/null
+++ b/spec/graphql/types/tree/submodule_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::Tree::SubmoduleType do
+ it { expect(described_class.graphql_name).to eq('Submodule') }
+
+ it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) }
+end
diff --git a/spec/graphql/types/tree/tree_entry_type_spec.rb b/spec/graphql/types/tree/tree_entry_type_spec.rb
new file mode 100644
index 00000000000..ea1b6426034
--- /dev/null
+++ b/spec/graphql/types/tree/tree_entry_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::Tree::TreeEntryType do
+ it { expect(described_class.graphql_name).to eq('TreeEntry') }
+
+ it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path, :web_url) }
+end
diff --git a/spec/graphql/types/tree/tree_type_spec.rb b/spec/graphql/types/tree/tree_type_spec.rb
new file mode 100644
index 00000000000..b9c5570115e
--- /dev/null
+++ b/spec/graphql/types/tree/tree_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::Tree::TreeType do
+ it { expect(described_class.graphql_name).to eq('Tree') }
+
+ it { expect(described_class).to have_graphql_fields(:trees, :submodules, :blobs) }
+end
diff --git a/spec/graphql/types/tree/type_enum_spec.rb b/spec/graphql/types/tree/type_enum_spec.rb
new file mode 100644
index 00000000000..4caf9e1c457
--- /dev/null
+++ b/spec/graphql/types/tree/type_enum_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Types::Tree::TypeEnum do
+ it { expect(described_class.graphql_name).to eq('EntryType') }
+
+ it 'exposes all tree entry types' do
+ expect(described_class.values.keys).to include(*%w[tree blob commit])
+ end
+end
diff --git a/spec/graphql/types/user_type_spec.rb b/spec/graphql/types/user_type_spec.rb
new file mode 100644
index 00000000000..8134cc13eb4
--- /dev/null
+++ b/spec/graphql/types/user_type_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['User'] do
+ it { expect(described_class.graphql_name).to eq('User') }
+
+ it { expect(described_class).to require_graphql_authorizations(:read_user) }
+end
diff --git a/spec/haml_lint/linter/no_plain_nodes_spec.rb b/spec/haml_lint/linter/no_plain_nodes_spec.rb
new file mode 100644
index 00000000000..08deb5a4e9e
--- /dev/null
+++ b/spec/haml_lint/linter/no_plain_nodes_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'haml_lint'
+require 'haml_lint/spec'
+require Rails.root.join('haml_lint/linter/no_plain_nodes')
+
+describe HamlLint::Linter::NoPlainNodes do
+ include_context 'linter'
+
+ context 'reports when a tag has an inline plain node' do
+ let(:haml) { '%tag Hello Tanuki' }
+ let(:message) { "`Hello Tanuki` is a plain node. Please use an i18n method like `= _('Hello Tanuki')`" }
+
+ it { is_expected.to report_lint message: message }
+ end
+
+ context 'reports when a tag has multiline plain nodes' do
+ let(:haml) { <<-HAML }
+ %tag
+ Hello
+ Tanuki
+ HAML
+
+ it { is_expected.to report_lint count: 1 }
+ end
+
+ context 'reports when a tag has an inline plain node with interpolation' do
+ let(:haml) { '%tag Hello #{"Tanuki"}!' } # rubocop:disable Lint/InterpolationCheck
+
+ it { is_expected.to report_lint }
+ end
+
+ context 'does not report when a tag has an inline script' do
+ let(:haml) { '%tag= "Hello Tanuki"' }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'does not report when a tag is empty' do
+ let(:haml) { '%tag' }
+
+ it { is_expected.not_to report_lint }
+ end
+
+ context 'reports multiple when a tag has multiline plain nodes split by non-text nodes' do
+ let(:haml) { <<-HAML }
+ %tag
+ Hello
+ .split-node There
+ Tanuki
+ HAML
+
+ it { is_expected.to report_lint count: 3 }
+ end
+end
diff --git a/spec/helpers/appearances_helper_spec.rb b/spec/helpers/appearances_helper_spec.rb
index 8d717b968dd..a3511e078ce 100644
--- a/spec/helpers/appearances_helper_spec.rb
+++ b/spec/helpers/appearances_helper_spec.rb
@@ -65,12 +65,10 @@ describe AppearancesHelper do
end
describe '#brand_title' do
- it 'returns the default CE title when no appearance is present' do
- allow(helper)
- .to receive(:current_appearance)
- .and_return(nil)
+ it 'returns the default title when no appearance is present' do
+ allow(helper).to receive(:current_appearance).and_return(nil)
- expect(helper.brand_title).to eq('GitLab Community Edition')
+ expect(helper.brand_title).to eq(helper.default_brand_title)
end
end
end
diff --git a/spec/helpers/auth_helper_spec.rb b/spec/helpers/auth_helper_spec.rb
index f0c2e4768ec..aae515def0c 100644
--- a/spec/helpers/auth_helper_spec.rb
+++ b/spec/helpers/auth_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe AuthHelper do
@@ -97,17 +99,37 @@ describe AuthHelper do
end
end
- describe 'unlink_allowed?' do
- [:saml, :cas3].each do |provider|
- it "returns true if the provider is #{provider}" do
- expect(helper.unlink_allowed?(provider)).to be false
- end
+ describe '#link_provider_allowed?' do
+ let(:policy) { instance_double('IdentityProviderPolicy') }
+ let(:current_user) { instance_double('User') }
+ let(:provider) { double }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(IdentityProviderPolicy).to receive(:new).with(current_user, provider).and_return(policy)
end
- [:twitter, :facebook, :google_oauth2, :gitlab, :github, :bitbucket, :crowd, :auth0, :authentiq].each do |provider|
- it "returns false if the provider is #{provider}" do
- expect(helper.unlink_allowed?(provider)).to be true
- end
+ it 'delegates to identity provider policy' do
+ allow(policy).to receive(:can?).with(:link).and_return('policy_link_result')
+
+ expect(helper.link_provider_allowed?(provider)).to eq 'policy_link_result'
+ end
+ end
+
+ describe '#unlink_provider_allowed?' do
+ let(:policy) { instance_double('IdentityProviderPolicy') }
+ let(:current_user) { instance_double('User') }
+ let(:provider) { double }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(current_user)
+ allow(IdentityProviderPolicy).to receive(:new).with(current_user, provider).and_return(policy)
+ end
+
+ it 'delegates to identity provider policy' do
+ allow(policy).to receive(:can?).with(:unlink).and_return('policy_unlink_result')
+
+ expect(helper.unlink_provider_allowed?(provider)).to eq 'policy_unlink_result'
end
end
end
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
index 223e562238d..d2540696b17 100644
--- a/spec/helpers/auto_devops_helper_spec.rb
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -29,11 +29,11 @@ describe AutoDevopsHelper do
end
context 'when the banner is disabled by feature flag' do
- it 'allows the feature flag to disable' do
+ before do
Feature.get(:auto_devops_banner_disabled).enable
-
- expect(subject).to be(false)
end
+
+ it { is_expected.to be_falsy }
end
context 'when dismissed' do
@@ -90,4 +90,136 @@ describe AutoDevopsHelper do
it { is_expected.to eq(false) }
end
end
+
+ describe '#badge_for_auto_devops_scope' do
+ subject { helper.badge_for_auto_devops_scope(receiver) }
+
+ context 'when receiver is a group' do
+ context 'when explicitly enabled' do
+ let(:receiver) { create(:group, :auto_devops_enabled) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+
+ context 'when explicitly disabled' do
+ let(:receiver) { create(:group, :auto_devops_disabled) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when auto devops is implicitly enabled' do
+ let(:receiver) { create(:group) }
+
+ context 'by instance' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq('instance enabled') }
+ end
+
+ context 'with groups', :nested_groups do
+ before do
+ receiver.update(parent: parent)
+ end
+
+ context 'when auto devops is enabled on parent' do
+ let(:parent) { create(:group, :auto_devops_enabled) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+
+ context 'when auto devops is enabled on parent group' do
+ let(:root_parent) { create(:group, :auto_devops_enabled) }
+ let(:parent) { create(:group, parent: root_parent) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+
+ context 'when auto devops disabled set on parent group' do
+ let(:root_parent) { create(:group, :auto_devops_disabled) }
+ let(:parent) { create(:group, parent: root_parent) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+
+ context 'when receiver is a project' do
+ context 'when auto devops is enabled at project level' do
+ let(:receiver) { create(:project, :auto_devops) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when auto devops is disabled at project level' do
+ let(:receiver) { create(:project, :auto_devops_disabled) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when auto devops is implicitly enabled' do
+ let(:receiver) { create(:project) }
+
+ context 'by instance' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq('instance enabled') }
+ end
+
+ context 'with groups', :nested_groups do
+ let(:receiver) { create(:project, :repository, namespace: group) }
+
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ context 'when auto devops is enabled on group level' do
+ let(:group) { create(:group, :auto_devops_enabled) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+
+ context 'when auto devops is enabled on root group' do
+ let(:root_parent) { create(:group, :auto_devops_enabled) }
+ let(:group) { create(:group, parent: root_parent) }
+
+ it { is_expected.to eq('group enabled') }
+ end
+ end
+ end
+
+ context 'when auto devops is implicitly disabled' do
+ let(:receiver) { create(:project) }
+
+ context 'by instance' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'with groups', :nested_groups do
+ let(:receiver) { create(:project, :repository, namespace: group) }
+
+ context 'when auto devops is disabled on group level' do
+ let(:group) { create(:group, :auto_devops_disabled) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when root group is enabled and parent disabled' do
+ let(:root_parent) { create(:group, :auto_devops_enabled) }
+ let(:group) { create(:group, :auto_devops_disabled, parent: root_parent) }
+
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index f709f152c92..6808ed86c9a 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -50,12 +50,20 @@ describe BlobHelper do
end
it 'returns a link with the proper route' do
+ stub_feature_flags(web_ide_default: false)
link = edit_blob_button(project, 'master', 'README.md')
expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/#{project.full_path}/edit/master/README.md")
end
+ it 'returns a link with a Web IDE route' do
+ link = edit_blob_button(project, 'master', 'README.md')
+
+ expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/-/ide/project/#{project.full_path}/edit/master/-/README.md")
+ end
+
it 'returns a link with the passed link_opts on the expected route' do
+ stub_feature_flags(web_ide_default: false)
link = edit_blob_button(project, 'master', 'README.md', link_opts: { mr_id: 10 })
expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/#{project.full_path}/edit/master/README.md?mr_id=10")
@@ -222,5 +230,18 @@ describe BlobHelper do
expect(helper.ide_edit_path(project, "master", "")).to eq("/gitlab/-/ide/project/#{project.namespace.path}/#{project.path}/edit/master")
end
+
+ it 'escapes special characters' do
+ Rails.application.routes.default_url_options[:script_name] = nil
+
+ expect(helper.ide_edit_path(project, "testing/#hashes", "readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/readme.md%23test")
+ expect(helper.ide_edit_path(project, "testing/#hashes", "src#/readme.md#test")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/#hashes/-/src%23/readme.md%23test")
+ end
+
+ it 'does not escape "/" character' do
+ Rails.application.routes.default_url_options[:script_name] = nil
+
+ expect(helper.ide_edit_path(project, "testing/slashes", "readme.md/")).to eq("/-/ide/project/#{project.namespace.path}/#{project.path}/edit/testing/slashes/-/readme.md/")
+ end
end
end
diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb
new file mode 100644
index 00000000000..4ea0f76fc28
--- /dev/null
+++ b/spec/helpers/clusters_helper_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ClustersHelper do
+ describe '#has_rbac_enabled?' do
+ context 'when kubernetes platform has been created' do
+ let(:platform_kubernetes) { build_stubbed(:cluster_platform_kubernetes) }
+ let(:cluster) { build_stubbed(:cluster, :provided_by_gcp, platform_kubernetes: platform_kubernetes) }
+
+ it 'returns kubernetes platform value' do
+ expect(helper.has_rbac_enabled?(cluster)).to be_truthy
+ end
+ end
+
+ context 'when kubernetes platform has not been created yet' do
+ let(:cluster) { build_stubbed(:cluster, :providing_by_gcp) }
+
+ it 'delegates to cluster provider' do
+ expect(helper.has_rbac_enabled?(cluster)).to be_truthy
+ end
+
+ context 'when ABAC cluster is created' do
+ let(:provider) { build_stubbed(:cluster_provider_gcp, :abac_enabled) }
+ let(:cluster) { build_stubbed(:cluster, :providing_by_gcp, provider_gcp: provider) }
+
+ it 'delegates to cluster provider' do
+ expect(helper.has_rbac_enabled?(cluster)).to be_falsy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/helpers/dashboard_helper_spec.rb b/spec/helpers/dashboard_helper_spec.rb
index 7ba24ba2956..023238ee0ae 100644
--- a/spec/helpers/dashboard_helper_spec.rb
+++ b/spec/helpers/dashboard_helper_spec.rb
@@ -21,4 +21,10 @@ describe DashboardHelper do
expect(helper.dashboard_nav_links).not_to include(:activity, :milestones)
end
end
+
+ describe '.has_start_trial?' do
+ subject { helper.has_start_trial? }
+
+ it { is_expected.to eq(false) }
+ end
end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 23d7e41803e..e6aacb5b92b 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -1,6 +1,45 @@
require 'spec_helper'
describe EmailsHelper do
+ describe 'closure_reason_text' do
+ context 'when given a MergeRequest' do
+ let(:merge_request) { create(:merge_request) }
+ let(:merge_request_presenter) { merge_request.present }
+
+ context "and format is text" do
+ it "returns plain text" do
+ expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ end
+ end
+
+ context "and format is HTML" do
+ it "returns HTML" do
+ expect(closure_reason_text(merge_request, format: :html)).to eq("via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}")
+ end
+ end
+
+ context "and format is unknown" do
+ it "returns plain text" do
+ expect(closure_reason_text(merge_request, format: :text)).to eq("via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ end
+ end
+ end
+
+ context 'when given a String' do
+ let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" }
+
+ it "returns plain text" do
+ expect(closure_reason_text(closed_via)).to eq("via #{closed_via}")
+ end
+ end
+
+ context 'when not given anything' do
+ it "returns empty string" do
+ expect(closure_reason_text(nil)).to eq("")
+ end
+ end
+ end
+
describe 'sanitize_name' do
context 'when name contains a valid URL string' do
it 'returns name with `.` replaced with `_` to prevent mail clients from auto-linking URLs' do
@@ -142,4 +181,58 @@ describe EmailsHelper do
end
end
end
+
+ describe 'header and footer messages' do
+ context 'when email_header_and_footer_enabled is enabled' do
+ it 'returns header and footer messages' do
+ create :appearance, header_message: 'Foo', footer_message: 'Bar', email_header_and_footer_enabled: true
+
+ aggregate_failures do
+ expect(html_header_message).to eq(%{<div class="header-message" style=""><p>Foo</p></div>})
+ expect(html_footer_message).to eq(%{<div class="footer-message" style=""><p>Bar</p></div>})
+ expect(text_header_message).to eq('Foo')
+ expect(text_footer_message).to eq('Bar')
+ end
+ end
+
+ context 'when header and footer messages are empty' do
+ it 'returns nil' do
+ create :appearance, header_message: '', footer_message: '', email_header_and_footer_enabled: true
+
+ aggregate_failures do
+ expect(html_header_message).to eq(nil)
+ expect(html_footer_message).to eq(nil)
+ expect(text_header_message).to eq(nil)
+ expect(text_footer_message).to eq(nil)
+ end
+ end
+ end
+
+ context 'when header and footer messages are nil' do
+ it 'returns nil' do
+ create :appearance, header_message: nil, footer_message: nil, email_header_and_footer_enabled: true
+
+ aggregate_failures do
+ expect(html_header_message).to eq(nil)
+ expect(html_footer_message).to eq(nil)
+ expect(text_header_message).to eq(nil)
+ expect(text_footer_message).to eq(nil)
+ end
+ end
+ end
+ end
+
+ context 'when email_header_and_footer_enabled is disabled' do
+ it 'returns header and footer messages' do
+ create :appearance, header_message: 'Foo', footer_message: 'Bar', email_header_and_footer_enabled: false
+
+ aggregate_failures do
+ expect(html_header_message).to eq(nil)
+ expect(html_footer_message).to eq(nil)
+ expect(text_header_message).to eq(nil)
+ expect(text_footer_message).to eq(nil)
+ end
+ end
+ end
+ end
end
diff --git a/spec/helpers/environments_helper_spec.rb b/spec/helpers/environments_helper_spec.rb
new file mode 100644
index 00000000000..0c8a8d2f032
--- /dev/null
+++ b/spec/helpers/environments_helper_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe EnvironmentsHelper do
+ set(:environment) { create(:environment) }
+ set(:project) { environment.project }
+ set(:user) { create(:user) }
+
+ describe '#metrics_data' do
+ before do
+ # This is so that this spec also passes in EE.
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).and_return(true)
+ end
+
+ let(:metrics_data) { helper.metrics_data(project, environment) }
+
+ it 'returns data' do
+ expect(metrics_data).to include(
+ 'settings-path' => edit_project_service_path(project, 'prometheus'),
+ 'clusters-path' => project_clusters_path(project),
+ 'current-environment-name': environment.name,
+ 'documentation-path' => help_page_path('administration/monitoring/prometheus/index.md'),
+ 'empty-getting-started-svg-path' => match_asset_path('/assets/illustrations/monitoring/getting_started.svg'),
+ 'empty-loading-svg-path' => match_asset_path('/assets/illustrations/monitoring/loading.svg'),
+ 'empty-no-data-svg-path' => match_asset_path('/assets/illustrations/monitoring/no_data.svg'),
+ 'empty-unable-to-connect-svg-path' => match_asset_path('/assets/illustrations/monitoring/unable_to_connect.svg'),
+ 'metrics-endpoint' => additional_metrics_project_environment_path(project, environment, format: :json),
+ 'deployment-endpoint' => project_environment_deployments_path(project, environment, format: :json),
+ 'environments-endpoint': project_environments_path(project, format: :json),
+ 'project-path' => project_path(project),
+ 'tags-path' => project_tags_path(project),
+ 'has-metrics' => "#{environment.has_metrics?}",
+ 'external-dashboard-url' => nil
+ )
+ end
+
+ context 'with metrics_setting' do
+ before do
+ create(:project_metrics_setting, project: project, external_dashboard_url: 'http://gitlab.com')
+ end
+
+ it 'adds external_dashboard_url' do
+ expect(metrics_data['external-dashboard-url']).to eq('http://gitlab.com')
+ end
+ end
+ end
+end
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 143b28728a3..027480143bd 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -101,7 +101,7 @@ describe GitlabRoutingHelper do
it 'returns project milestone edit path when given entity parent is not a Group' do
milestone = create(:milestone, group: nil)
- expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/milestones/#{milestone.iid}/edit")
+ expect(edit_milestone_path(milestone)).to eq("/#{milestone.project.full_path}/-/milestones/#{milestone.iid}/edit")
end
end
end
diff --git a/spec/helpers/groups/group_members_helper_spec.rb b/spec/helpers/groups/group_members_helper_spec.rb
new file mode 100644
index 00000000000..898c330c498
--- /dev/null
+++ b/spec/helpers/groups/group_members_helper_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe Groups::GroupMembersHelper do
+ describe '.group_member_select_options' do
+ let(:group) { create(:group) }
+
+ before do
+ helper.instance_variable_set(:@group, group)
+ end
+
+ it 'returns an options hash' do
+ expect(helper.group_member_select_options).to include(multiple: true, scope: :all, email_user: true)
+ end
+ end
+end
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 540a8674ec2..1763c46389a 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupsHelper do
@@ -227,4 +229,37 @@ describe GroupsHelper do
expect(helper.group_sidebar_links).not_to include(*cross_project_features)
end
end
+
+ describe 'parent_group_options', :nested_groups do
+ let(:current_user) { create(:user) }
+ let(:group) { create(:group, name: 'group') }
+ let(:group2) { create(:group, name: 'group2') }
+
+ before do
+ group.add_owner(current_user)
+ group2.add_owner(current_user)
+ end
+
+ it 'includes explicitly owned groups except self' do
+ expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json)
+ end
+
+ it 'excludes parent group' do
+ subgroup = create(:group, parent: group2)
+
+ expect(parent_group_options(subgroup)).to eq([{ id: group.id, text: group.human_name }].to_json)
+ end
+
+ it 'includes subgroups with inherited ownership' do
+ subgroup = create(:group, parent: group)
+
+ expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }, { id: subgroup.id, text: subgroup.human_name }].to_json)
+ end
+
+ it 'excludes own subgroups' do
+ create(:group, parent: group2)
+
+ expect(parent_group_options(group2)).to eq([{ id: group.id, text: group.human_name }].to_json)
+ end
+ end
end
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 4b40d523287..37e9ddadb8c 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -59,19 +59,19 @@ describe IconsHelper do
describe 'non existing icon' do
non_existing = 'non_existing_icon_sprite'
- it 'should raise in development mode' do
+ it 'raises in development mode' do
allow(Rails.env).to receive(:development?).and_return(true)
expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/)
end
- it 'should raise in test mode' do
+ it 'raises in test mode' do
allow(Rails.env).to receive(:test?).and_return(true)
expect { sprite_icon(non_existing) }.to raise_error(ArgumentError, /is not a known icon/)
end
- it 'should not raise in production mode' do
+ it 'does not raise in production mode' do
allow(Rails.env).to receive(:test?).and_return(false)
allow(Rails.env).to receive(:development?).and_return(false)
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 8b82dea2524..1d1446eaa30 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssuablesHelper do
@@ -176,7 +178,7 @@ describe IssuablesHelper do
stub_commonmark_sourcepos_disabled
end
- it 'returns the correct json for an issue' do
+ it 'returns the correct data for an issue' do
issue = create(:issue, author: user, description: 'issue text')
@project = issue.project
@@ -198,7 +200,7 @@ describe IssuablesHelper do
initialDescriptionText: 'issue text',
initialTaskStatus: '0 of 0 tasks completed'
}
- expect(helper.issuable_initial_data(issue)).to eq(expected_data)
+ expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data))
end
end
end
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 012678db9c2..314305d7a8e 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -6,7 +6,7 @@ describe LabelsHelper do
let(:context_project) { project }
context "when asking for a #{issuables_type} link" do
- subject { show_label_issuables_link?(label, issuables_type, project: context_project) }
+ subject { show_label_issuables_link?(label.present(issuable_subject: nil), issuables_type, project: context_project) }
context "when #{issuables_type} are enabled for the project" do
let(:project) { create(:project, "#{issuables_type}_access_level": ProjectFeature::ENABLED) }
@@ -67,27 +67,29 @@ describe LabelsHelper do
describe 'link_to_label' do
let(:project) { create(:project) }
let(:label) { create(:label, project: project) }
+ let(:subject) { nil }
+ let(:label_presenter) { label.present(issuable_subject: subject) }
context 'without subject' do
it "uses the label's project" do
- expect(link_to_label(label)).to match %r{<a href="/#{label.project.full_path}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label_presenter)).to match %r{<a href="/#{label.project.full_path}/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
context 'with a project as subject' do
let(:namespace) { build(:namespace, name: 'foo3') }
- let(:another_project) { build(:project, namespace: namespace, name: 'bar3') }
+ let(:subject) { build(:project, namespace: namespace, name: 'bar3') }
it 'links to project issues page' do
- expect(link_to_label(label, subject: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label_presenter)).to match %r{<a href="/foo3/bar3/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
context 'with a group as subject' do
- let(:group) { build(:group, name: 'bar') }
+ let(:subject) { build(:group, name: 'bar') }
it 'links to group issues page' do
- expect(link_to_label(label, subject: group)).to match %r{<a href="/groups/bar/-/issues\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label_presenter)).to match %r{<a href="/groups/bar/-/issues\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
@@ -95,7 +97,7 @@ describe LabelsHelper do
['issue', :issue, 'merge_request', :merge_request].each do |type|
context "set to #{type}" do
it 'links to correct page' do
- expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.full_path}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>}
+ expect(link_to_label(label_presenter, type: type)).to match %r{<a href="/#{label.project.full_path}/#{type.to_s.pluralize}\?label_name%5B%5D=#{label.name}">.*</a>}
end
end
end
@@ -104,14 +106,14 @@ describe LabelsHelper do
context 'with a tooltip argument' do
context 'set to false' do
it 'does not include the has-tooltip class' do
- expect(link_to_label(label, tooltip: false)).not_to match /has-tooltip/
+ expect(link_to_label(label_presenter, tooltip: false)).not_to match /has-tooltip/
end
end
end
context 'with block' do
it 'passes the block to link_to' do
- link = link_to_label(label) { 'Foo' }
+ link = link_to_label(label_presenter) { 'Foo' }
expect(link).to match('Foo')
end
end
@@ -119,8 +121,8 @@ describe LabelsHelper do
context 'without block' do
it 'uses render_colored_label as the link content' do
expect(self).to receive(:render_colored_label)
- .with(label, tooltip: true).and_return('Foo')
- expect(link_to_label(label)).to match('Foo')
+ .with(label_presenter, tooltip: true).and_return('Foo')
+ expect(link_to_label(label_presenter)).to match('Foo')
end
end
end
@@ -237,16 +239,61 @@ describe LabelsHelper do
end
end
- describe 'labels_sorted_by_title' do
+ describe 'presented_labels_sorted_by_title' do
+ let(:labels) do
+ [build(:label, title: 'a'),
+ build(:label, title: 'B'),
+ build(:label, title: 'c'),
+ build(:label, title: 'D')]
+ end
+
it 'sorts labels alphabetically' do
- label1 = double(:label, title: 'a')
- label2 = double(:label, title: 'B')
- label3 = double(:label, title: 'c')
- label4 = double(:label, title: 'D')
- labels = [label1, label2, label3, label4]
-
- expect(labels_sorted_by_title(labels))
- .to match_array([label2, label4, label1, label3])
+ sorted_ids = presented_labels_sorted_by_title(labels, nil).map(&:id)
+
+ expect(sorted_ids)
+ .to match_array([labels[1].id, labels[3].id, labels[0].id, labels[2].id])
+ end
+
+ it 'returns an array of label presenters' do
+ expect(presented_labels_sorted_by_title(labels, nil))
+ .to all(be_a(LabelPresenter))
+ end
+ end
+
+ describe 'label_from_hash' do
+ it 'builds a group label with whitelisted attributes' do
+ label = label_from_hash({ title: 'foo', color: 'bar', id: 1, group_id: 1 })
+
+ expect(label).to be_a(GroupLabel)
+ expect(label.id).to be_nil
+ expect(label.title).to eq('foo')
+ expect(label.color).to eq('bar')
+ end
+
+ it 'builds a project label with whitelisted attributes' do
+ label = label_from_hash({ title: 'foo', color: 'bar', id: 1, project_id: 1 })
+
+ expect(label).to be_a(ProjectLabel)
+ expect(label.id).to be_nil
+ expect(label.title).to eq('foo')
+ expect(label.color).to eq('bar')
+ end
+ end
+
+ describe '#label_status_tooltip' do
+ let(:status) { 'unsubscribed'.inquiry }
+ subject { label_status_tooltip(label.present(issuable_subject: nil), status) }
+
+ context 'with a project label' do
+ let(:label) { create(:label, title: 'bug') }
+
+ it { is_expected.to eq('Subscribe at project level') }
+ end
+
+ context 'with a group label' do
+ let(:label) { create(:group_label, title: 'bug') }
+
+ it { is_expected.to eq('Subscribe at group level') }
end
end
end
diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb
index c3956ba08fd..597c8f836a9 100644
--- a/spec/helpers/markup_helper_spec.rb
+++ b/spec/helpers/markup_helper_spec.rb
@@ -78,7 +78,8 @@ describe MarkupHelper do
let(:link) { '/commits/0a1b2c3d' }
let(:issues) { create_list(:issue, 2, project: project) }
- it 'handles references nested in links with all the text' do
+ # Clean the cache to make sure the title is re-rendered from the stubbed one
+ it 'handles references nested in links with all the text', :clean_gitlab_redis_cache do
allow(commit).to receive(:title).and_return("This should finally fix #{issues[0].to_reference} and #{issues[1].to_reference} for real")
actual = helper.link_to_markdown_field(commit, :title, link)
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 885204062fe..193390d2f2c 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestsHelper do
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 7ccbdcd1332..601f864ef36 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -1,10 +1,38 @@
require 'spec_helper'
-describe NamespacesHelper do
+describe NamespacesHelper, :postgresql do
let!(:admin) { create(:admin) }
- let!(:admin_group) { create(:group, :private) }
+ let!(:admin_project_creation_level) { nil }
+ let!(:admin_group) do
+ create(:group,
+ :private,
+ project_creation_level: admin_project_creation_level)
+ end
let!(:user) { create(:user) }
- let!(:user_group) { create(:group, :private) }
+ let!(:user_project_creation_level) { nil }
+ let!(:user_group) do
+ create(:group,
+ :private,
+ project_creation_level: user_project_creation_level)
+ end
+ let!(:subgroup1) do
+ create(:group,
+ :private,
+ parent: admin_group,
+ project_creation_level: nil)
+ end
+ let!(:subgroup2) do
+ create(:group,
+ :private,
+ parent: admin_group,
+ project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ end
+ let!(:subgroup3) do
+ create(:group,
+ :private,
+ parent: admin_group,
+ project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ end
before do
admin_group.add_owner(admin)
@@ -105,5 +133,43 @@ describe NamespacesHelper do
helper.namespaces_options
end
end
+
+ describe 'include_groups_with_developer_maintainer_access parameter' do
+ context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set for a project' do
+ let!(:admin_project_creation_level) { ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS }
+
+ it 'returns groups where user is a developer' do
+ allow(helper).to receive(:current_user).and_return(user)
+ stub_application_setting(default_project_creation: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS)
+ admin_group.add_user(user, GroupMember::DEVELOPER)
+
+ options = helper.namespaces_options_with_developer_maintainer_access
+
+ expect(options).to include(admin_group.name)
+ expect(options).not_to include(subgroup1.name)
+ expect(options).to include(subgroup2.name)
+ expect(options).not_to include(subgroup3.name)
+ expect(options).to include(user_group.name)
+ expect(options).to include(user.name)
+ end
+ end
+
+ context 'when DEVELOPER_MAINTAINER_PROJECT_ACCESS is set globally' do
+ it 'return groups where default is not overridden' do
+ allow(helper).to receive(:current_user).and_return(user)
+ stub_application_setting(default_project_creation: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
+ admin_group.add_user(user, GroupMember::DEVELOPER)
+
+ options = helper.namespaces_options_with_developer_maintainer_access
+
+ expect(options).to include(admin_group.name)
+ expect(options).to include(subgroup1.name)
+ expect(options).to include(subgroup2.name)
+ expect(options).not_to include(subgroup3.name)
+ expect(options).to include(user_group.name)
+ expect(options).to include(user.name)
+ end
+ end
+ end
end
end
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index e840c927d59..979d89812f5 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -50,4 +50,16 @@ describe NavHelper do
expect(helper.header_links).to contain_exactly(:sign_in, :search)
end
end
+
+ context '.admin_monitoring_nav_links' do
+ subject { helper.admin_monitoring_nav_links }
+
+ it { is_expected.to all(be_a(String)) }
+ end
+
+ context '.group_issues_sub_menu_items' do
+ subject { helper.group_issues_sub_menu_items }
+
+ it { is_expected.to all(be_a(String)) }
+ end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index cf98eed27f1..bf50763d06f 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -38,6 +38,14 @@ describe PageLayoutHelper do
expect(helper.page_description).to eq 'Bold Header'
end
+
+ it 'truncates before sanitizing' do
+ helper.page_description('<b>Bold</b> <img> <img> <img> <h1>Header</h1> ' * 10)
+
+ # 12 words because <img> was counted as a word
+ expect(helper.page_description)
+ .to eq('Bold Header Bold Header Bold Header Bold Header Bold Header Bold Header...')
+ end
end
describe 'page_image' do
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index e0e8ebd0c3c..db0d45c3692 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -36,10 +36,11 @@ describe PreferencesHelper do
end
describe '#first_day_of_week_choices' do
- it 'returns Sunday and Monday as choices' do
+ it 'returns Saturday, Sunday and Monday as choices' do
expect(helper.first_day_of_week_choices).to eq [
['Sunday', 0],
- ['Monday', 1]
+ ['Monday', 1],
+ ['Saturday', 6]
]
end
end
@@ -47,14 +48,21 @@ describe PreferencesHelper do
describe '#first_day_of_week_choices_with_default' do
it 'returns choices including system default' do
expect(helper.first_day_of_week_choices_with_default).to eq [
- ['System default (Sunday)', nil], ['Sunday', 0], ['Monday', 1]
+ ['System default (Sunday)', nil], ['Sunday', 0], ['Monday', 1], ['Saturday', 6]
]
end
it 'returns choices including system default set to Monday' do
stub_application_setting(first_day_of_week: 1)
expect(helper.first_day_of_week_choices_with_default).to eq [
- ['System default (Monday)', nil], ['Sunday', 0], ['Monday', 1]
+ ['System default (Monday)', nil], ['Sunday', 0], ['Monday', 1], ['Saturday', 6]
+ ]
+ end
+
+ it 'returns choices including system default set to Saturday' do
+ stub_application_setting(first_day_of_week: 6)
+ expect(helper.first_day_of_week_choices_with_default).to eq [
+ ['System default (Saturday)', nil], ['Sunday', 0], ['Monday', 1], ['Saturday', 6]
]
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 49895b0680b..3716879c458 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -1,8 +1,60 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectsHelper do
include ProjectForksHelper
+ describe '#error_tracking_setting_project_json' do
+ let(:project) { create(:project) }
+
+ context 'error tracking setting does not exist' do
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ it 'returns nil' do
+ expect(helper.error_tracking_setting_project_json).to be_nil
+ end
+ end
+
+ context 'error tracking setting exists' do
+ let!(:error_tracking_setting) { create(:project_error_tracking_setting, project: project) }
+
+ context 'api_url present' do
+ let(:json) do
+ {
+ name: error_tracking_setting.project_name,
+ organization_name: error_tracking_setting.organization_name,
+ organization_slug: error_tracking_setting.organization_slug,
+ slug: error_tracking_setting.project_slug
+ }.to_json
+ end
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ it 'returns error tracking json' do
+ expect(helper.error_tracking_setting_project_json).to eq(json)
+ end
+ end
+
+ context 'api_url not present' do
+ before do
+ project.error_tracking_setting.api_url = nil
+ project.error_tracking_setting.enabled = false
+
+ helper.instance_variable_set(:@project, project)
+ end
+
+ it 'returns nil' do
+ expect(helper.error_tracking_setting_project_json).to be_nil
+ end
+ end
+ end
+ end
+
describe "#project_status_css_class" do
it "returns appropriate class" do
expect(project_status_css_class("started")).to eq("table-active")
@@ -393,6 +445,10 @@ describe ProjectsHelper do
Project.all
end
+ before do
+ stub_feature_flags(project_list_filter_bar: false)
+ end
+
it 'returns true when there are projects' do
expect(helper.show_projects?(projects, {})).to eq(true)
end
@@ -743,4 +799,46 @@ describe ProjectsHelper do
it { is_expected.to eq(result) }
end
end
+
+ describe '#can_import_members?' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:owner) { project.owner }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ it 'returns false if user cannot admin_project_member' do
+ allow(helper).to receive(:current_user) { user }
+ expect(helper.can_import_members?).to eq false
+ end
+
+ it 'returns true if user can admin_project_member' do
+ allow(helper).to receive(:current_user) { owner }
+ expect(helper.can_import_members?).to eq true
+ end
+ end
+
+ describe '#metrics_external_dashboard_url' do
+ let(:project) { create(:project) }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ context 'metrics_setting exists' do
+ it 'returns external_dashboard_url' do
+ metrics_setting = create(:project_metrics_setting, project: project)
+
+ expect(helper.metrics_external_dashboard_url).to eq(metrics_setting.external_dashboard_url)
+ end
+ end
+
+ context 'metrics_setting does not exist' do
+ it 'returns nil' do
+ expect(helper.metrics_external_dashboard_url).to eq(nil)
+ end
+ end
+ end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index 9cff0291250..2f59cfda0a0 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -12,7 +12,7 @@ describe SearchHelper do
allow(self).to receive(:current_user).and_return(nil)
end
- it "it returns nil" do
+ it "returns nil" do
expect(search_autocomplete_opts("q")).to be_nil
end
end
diff --git a/spec/helpers/storage_helper_spec.rb b/spec/helpers/storage_helper_spec.rb
index 03df9deafa1..62c00964524 100644
--- a/spec/helpers/storage_helper_spec.rb
+++ b/spec/helpers/storage_helper_spec.rb
@@ -18,4 +18,29 @@ describe StorageHelper do
expect(helper.storage_counter(100_000_000_000_000_000_000_000)).to eq("86,736.2 EB")
end
end
+
+ describe "#storage_counters_details" do
+ let(:namespace) { create :namespace }
+ let(:project) do
+ create(:project,
+ namespace: namespace,
+ statistics: build(:project_statistics,
+ repository_size: 10.kilobytes,
+ wiki_size: 10.bytes,
+ lfs_objects_size: 20.gigabytes,
+ build_artifacts_size: 30.megabytes))
+ end
+
+ let(:message) { '10 KB repositories, 10 Bytes wikis, 30 MB build artifacts, 20 GB LFS' }
+
+ it 'works on ProjectStatistics' do
+ expect(helper.storage_counters_details(project.statistics)).to eq(message)
+ end
+
+ it 'works on Namespace.with_statistics' do
+ namespace_stats = Namespace.with_statistics.find(project.namespace.id)
+
+ expect(helper.storage_counters_details(namespace_stats)).to eq(message)
+ end
+ end
end
diff --git a/spec/helpers/tracking_helper_spec.rb b/spec/helpers/tracking_helper_spec.rb
new file mode 100644
index 00000000000..71505e8ea69
--- /dev/null
+++ b/spec/helpers/tracking_helper_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TrackingHelper do
+ describe '#tracking_attrs' do
+ it 'returns an empty hash' do
+ expect(helper.tracking_attrs('a', 'b', 'c')).to eq({})
+ end
+ end
+end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index bfec7ad4bba..e384e2bf9a0 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe VersionCheckHelper do
describe '#version_status_badge' do
- it 'should return nil if not dev environment and not enabled' do
+ it 'returns nil if not dev environment and not enabled' do
allow(Rails.env).to receive(:production?) { false }
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false }
@@ -16,16 +16,16 @@ describe VersionCheckHelper do
allow(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
end
- it 'should return an image tag' do
+ it 'returns an image tag' do
expect(helper.version_status_badge).to start_with('<img')
end
- it 'should have a js prefixed css class' do
+ it 'has a js prefixed css class' do
expect(helper.version_status_badge)
.to match(/class="js-version-status-badge lazy"/)
end
- it 'should have a VersionCheck url as the src' do
+ it 'has a VersionCheck url as the src' do
expect(helper.version_status_badge)
.to include(%{src="https://version.host.com/check.svg?gitlab_info=xxx"})
end
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index e565ac8c530..25a2fcf5a81 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -162,4 +162,49 @@ describe VisibilityLevelHelper do
end
end
end
+
+ describe "selected_visibility_level" do
+ let(:group) { create(:group, :public) }
+ let!(:project) { create(:project, :internal, group: group) }
+ let!(:forked_project) { fork_project(project) }
+
+ using RSpec::Parameterized::TableSyntax
+
+ PUBLIC = Gitlab::VisibilityLevel::PUBLIC
+ INTERNAL = Gitlab::VisibilityLevel::INTERNAL
+ PRIVATE = Gitlab::VisibilityLevel::PRIVATE
+
+ # This is a subset of all the permutations
+ where(:requested_level, :max_allowed, :global_default_level, :restricted_levels, :expected) do
+ PUBLIC | PUBLIC | PUBLIC | [] | PUBLIC
+ PUBLIC | PUBLIC | PUBLIC | [PUBLIC] | INTERNAL
+ INTERNAL | PUBLIC | PUBLIC | [] | INTERNAL
+ INTERNAL | PRIVATE | PRIVATE | [] | PRIVATE
+ PRIVATE | PUBLIC | PUBLIC | [] | PRIVATE
+ PUBLIC | PRIVATE | INTERNAL | [] | PRIVATE
+ PUBLIC | INTERNAL | PUBLIC | [] | INTERNAL
+ PUBLIC | PRIVATE | PUBLIC | [] | PRIVATE
+ PUBLIC | INTERNAL | INTERNAL | [] | INTERNAL
+ PUBLIC | PUBLIC | INTERNAL | [] | PUBLIC
+ end
+
+ before do
+ stub_application_setting(restricted_visibility_levels: restricted_levels,
+ default_project_visibility: global_default_level)
+ end
+
+ with_them do
+ it "provides correct visibility level for forked project" do
+ project.update(visibility_level: max_allowed)
+
+ expect(selected_visibility_level(forked_project, requested_level)).to eq(expected)
+ end
+
+ it "provides correct visibiility level for project in group" do
+ project.group.update(visibility_level: max_allowed)
+
+ expect(selected_visibility_level(project, requested_level)).to eq(expected)
+ end
+ end
+ end
end
diff --git a/spec/helpers/wiki_helper_spec.rb b/spec/helpers/wiki_helper_spec.rb
index 92c6f27a867..8eab40aeaf3 100644
--- a/spec/helpers/wiki_helper_spec.rb
+++ b/spec/helpers/wiki_helper_spec.rb
@@ -18,4 +18,56 @@ describe WikiHelper do
end
end
end
+
+ describe '#wiki_sort_controls' do
+ let(:project) { create(:project) }
+ let(:wiki_link) { helper.wiki_sort_controls(project, sort, direction) }
+ let(:classes) { "btn btn-default has-tooltip reverse-sort-btn qa-reverse-sort" }
+
+ def expected_link(sort, direction, icon_class)
+ path = "/#{project.full_path}/wikis/pages?direction=#{direction}&sort=#{sort}"
+
+ helper.link_to(path, type: 'button', class: classes, title: 'Sort direction') do
+ helper.sprite_icon("sort-#{icon_class}", size: 16)
+ end
+ end
+
+ context 'initial call' do
+ let(:sort) { nil }
+ let(:direction) { nil }
+
+ it 'renders with default values' do
+ expect(wiki_link).to eq(expected_link('title', 'desc', 'lowest'))
+ end
+ end
+
+ context 'sort by title' do
+ let(:sort) { 'title' }
+ let(:direction) { 'asc' }
+
+ it 'renders a link with opposite direction' do
+ expect(wiki_link).to eq(expected_link('title', 'desc', 'lowest'))
+ end
+ end
+
+ context 'sort by created_at' do
+ let(:sort) { 'created_at' }
+ let(:direction) { 'desc' }
+
+ it 'renders a link with opposite direction' do
+ expect(wiki_link).to eq(expected_link('created_at', 'asc', 'highest'))
+ end
+ end
+ end
+
+ describe '#wiki_sort_title' do
+ it 'returns a title corresponding to a key' do
+ expect(helper.wiki_sort_title('created_at')).to eq('Created date')
+ expect(helper.wiki_sort_title('title')).to eq('Title')
+ end
+
+ it 'defaults to Title if a key is unknown' do
+ expect(helper.wiki_sort_title('unknown')).to eq('Title')
+ end
+ end
end
diff --git a/spec/initializers/secret_token_spec.rb b/spec/initializers/secret_token_spec.rb
index 6366be30079..726ce07a2d1 100644
--- a/spec/initializers/secret_token_spec.rb
+++ b/spec/initializers/secret_token_spec.rb
@@ -6,8 +6,8 @@ describe 'create_tokens' do
let(:secrets) { ActiveSupport::OrderedOptions.new }
- HEX_KEY = /\h{128}/
- RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m
+ HEX_KEY = /\h{128}/.freeze
+ RSA_KEY = /\A-----BEGIN RSA PRIVATE KEY-----\n.+\n-----END RSA PRIVATE KEY-----\n\Z/m.freeze
before do
allow(File).to receive(:write)
diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js
index 9389fc94f17..89195a4397f 100644
--- a/spec/javascripts/ajax_loading_spinner_spec.js
+++ b/spec/javascripts/ajax_loading_spinner_spec.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import AjaxLoadingSpinner from '~/ajax_loading_spinner';
describe('Ajax Loading Spinner', () => {
- const fixtureTemplate = 'static/ajax_loading_spinner.html.raw';
+ const fixtureTemplate = 'static/ajax_loading_spinner.html';
preloadFixtures(fixtureTemplate);
beforeEach(() => {
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index ce5d2022441..02200f77ad7 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -19,9 +19,9 @@ const lazyAssert = function(done, assertFn) {
};
describe('AwardsHandler', function() {
- preloadFixtures('snippets/show.html.raw');
+ preloadFixtures('snippets/show.html');
beforeEach(function(done) {
- loadFixtures('snippets/show.html.raw');
+ loadFixtures('snippets/show.html');
loadAwardsHandler(true)
.then(obj => {
awardsHandler = obj;
diff --git a/spec/javascripts/badges/components/badge_list_spec.js b/spec/javascripts/badges/components/badge_list_spec.js
index 536671db377..2f72c9ed89d 100644
--- a/spec/javascripts/badges/components/badge_list_spec.js
+++ b/spec/javascripts/badges/components/badge_list_spec.js
@@ -60,7 +60,7 @@ describe('BadgeList component', () => {
Vue.nextTick()
.then(() => {
- const loadingIcon = vm.$el.querySelector('.fa-spinner');
+ const loadingIcon = vm.$el.querySelector('.spinner');
expect(loadingIcon).toBeVisible();
})
diff --git a/spec/javascripts/badges/components/badge_spec.js b/spec/javascripts/badges/components/badge_spec.js
index 29805408bcf..4e4d1ae2e99 100644
--- a/spec/javascripts/badges/components/badge_spec.js
+++ b/spec/javascripts/badges/components/badge_spec.js
@@ -15,7 +15,7 @@ describe('Badge component', () => {
const buttons = vm.$el.querySelectorAll('button');
return {
badgeImage: vm.$el.querySelector('img.project-badge'),
- loadingIcon: vm.$el.querySelector('.fa-spinner'),
+ loadingIcon: vm.$el.querySelector('.spinner'),
reloadButton: buttons[buttons.length - 1],
};
};
diff --git a/spec/javascripts/badges/store/actions_spec.js b/spec/javascripts/badges/store/actions_spec.js
index 2623465ebd6..e8d5f8c3aac 100644
--- a/spec/javascripts/badges/store/actions_spec.js
+++ b/spec/javascripts/badges/store/actions_spec.js
@@ -411,7 +411,7 @@ describe('Badges store actions', () => {
it('escapes user input', done => {
spyOn(axios, 'get').and.callFake(() => Promise.resolve({ data: createDummyBadgeResponse() }));
- badgeInForm.imageUrl = '&make-sandwhich=true';
+ badgeInForm.imageUrl = '&make-sandwich=true';
badgeInForm.linkUrl = '<script>I am dangerous!</script>';
actions
@@ -422,7 +422,7 @@ describe('Badges store actions', () => {
expect(url).toMatch(`^${dummyEndpointUrl}/render?`);
expect(url).toMatch('\\?link_url=%3Cscript%3EI%20am%20dangerous!%3C%2Fscript%3E&');
- expect(url).toMatch('&image_url=%26make-sandwhich%3Dtrue$');
+ expect(url).toMatch('&image_url=%26make-sandwich%3Dtrue$');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/behaviors/copy_as_gfm_spec.js b/spec/javascripts/behaviors/copy_as_gfm_spec.js
index ca849f75860..d653fca0988 100644
--- a/spec/javascripts/behaviors/copy_as_gfm_spec.js
+++ b/spec/javascripts/behaviors/copy_as_gfm_spec.js
@@ -100,7 +100,7 @@ describe('CopyAsGFM', () => {
simulateCopy();
setTimeout(() => {
- const expectedGFM = '* List Item1\n\n* List Item2';
+ const expectedGFM = '* List Item1\n* List Item2';
expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
done();
@@ -114,7 +114,7 @@ describe('CopyAsGFM', () => {
simulateCopy();
setTimeout(() => {
- const expectedGFM = '1. List Item1\n\n1. List Item2';
+ const expectedGFM = '1. List Item1\n1. List Item2';
expect(clipboardData.setData).toHaveBeenCalledWith('text/x-gfm', expectedGFM);
done();
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 681463aab66..7af8c984841 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -4,10 +4,10 @@ import '~/behaviors/quick_submit';
describe('Quick Submit behavior', function() {
const keydownEvent = (options = { keyCode: 13, metaKey: true }) => $.Event('keydown', options);
- preloadFixtures('snippets/show.html.raw');
+ preloadFixtures('snippets/show.html');
beforeEach(() => {
- loadFixtures('snippets/show.html.raw');
+ loadFixtures('snippets/show.html');
$('form').submit(e => {
// Prevent a form submit from moving us off the testing page
e.preventDefault();
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 1bde2bb3024..617fe49b059 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -3,10 +3,10 @@ import '~/behaviors/requires_input';
describe('requiresInput', () => {
let submitButton;
- preloadFixtures('branches/new_branch.html.raw');
+ preloadFixtures('branches/new_branch.html');
beforeEach(() => {
- loadFixtures('branches/new_branch.html.raw');
+ loadFixtures('branches/new_branch.html');
submitButton = $('button[type="submit"]');
});
diff --git a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
index 4843a0386b5..5e457a4e823 100644
--- a/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js
@@ -9,7 +9,7 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
const FORM_SELECTOR = '.js-main-target-form .js-vue-comment-form';
describe('ShortcutsIssuable', function() {
- const fixtureName = 'snippets/show.html.raw';
+ const fixtureName = 'snippets/show.html';
preloadFixtures(fixtureName);
beforeAll(done => {
diff --git a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
index 5f027f59fcf..33210794ba1 100644
--- a/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
+++ b/spec/javascripts/blob/balsamiq/balsamiq_viewer_integration_spec.js
@@ -1,15 +1,17 @@
import BalsamiqViewer from '~/blob/balsamiq/balsamiq_viewer';
-import bmprPath from '../../fixtures/blob/balsamiq/test.bmpr';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const bmprPath = `${FIXTURES_PATH}/blob/balsamiq/test.bmpr`;
describe('Balsamiq integration spec', () => {
let container;
let endpoint;
let balsamiqViewer;
- preloadFixtures('static/balsamiq_viewer.html.raw');
+ preloadFixtures('static/balsamiq_viewer.html');
beforeEach(() => {
- loadFixtures('static/balsamiq_viewer.html.raw');
+ loadFixtures('static/balsamiq_viewer.html');
container = document.getElementById('js-balsamiq-viewer');
balsamiqViewer = new BalsamiqViewer(container);
diff --git a/spec/javascripts/blob/blob_file_dropzone_spec.js b/spec/javascripts/blob/blob_file_dropzone_spec.js
index 432d8a65b0a..cab06a0a9be 100644
--- a/spec/javascripts/blob/blob_file_dropzone_spec.js
+++ b/spec/javascripts/blob/blob_file_dropzone_spec.js
@@ -2,10 +2,10 @@ import $ from 'jquery';
import BlobFileDropzone from '~/blob/blob_file_dropzone';
describe('BlobFileDropzone', function() {
- preloadFixtures('blob/show.html.raw');
+ preloadFixtures('blob/show.html');
beforeEach(() => {
- loadFixtures('blob/show.html.raw');
+ loadFixtures('blob/show.html');
const form = $('.js-upload-blob-form');
this.blobFileDropzone = new BlobFileDropzone(form, 'POST');
this.dropzone = $('.js-upload-blob-form .dropzone').get(0).dropzone;
diff --git a/spec/javascripts/blob/notebook/index_spec.js b/spec/javascripts/blob/notebook/index_spec.js
index 28d3b2f5ea3..6bb5bac007f 100644
--- a/spec/javascripts/blob/notebook/index_spec.js
+++ b/spec/javascripts/blob/notebook/index_spec.js
@@ -3,10 +3,10 @@ import axios from '~/lib/utils/axios_utils';
import renderNotebook from '~/blob/notebook';
describe('iPython notebook renderer', () => {
- preloadFixtures('static/notebook_viewer.html.raw');
+ preloadFixtures('static/notebook_viewer.html');
beforeEach(() => {
- loadFixtures('static/notebook_viewer.html.raw');
+ loadFixtures('static/notebook_viewer.html');
});
it('shows loading icon', () => {
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index be917a0613f..6fa3890483c 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -1,5 +1,7 @@
import renderPDF from '~/blob/pdf';
-import testPDF from '../../fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('PDF renderer', () => {
let viewer;
@@ -15,10 +17,10 @@ describe('PDF renderer', () => {
}
};
- preloadFixtures('static/pdf_viewer.html.raw');
+ preloadFixtures('static/pdf_viewer.html');
beforeEach(() => {
- loadFixtures('static/pdf_viewer.html.raw');
+ loadFixtures('static/pdf_viewer.html');
viewer = document.getElementById('js-pdf-viewer');
viewer.dataset.endpoint = testPDF;
});
diff --git a/spec/javascripts/blob/sketch/index_spec.js b/spec/javascripts/blob/sketch/index_spec.js
index 2b1e81e9cbc..3d3129e10da 100644
--- a/spec/javascripts/blob/sketch/index_spec.js
+++ b/spec/javascripts/blob/sketch/index_spec.js
@@ -13,10 +13,10 @@ describe('Sketch viewer', () => {
});
};
- preloadFixtures('static/sketch_viewer.html.raw');
+ preloadFixtures('static/sketch_viewer.html');
beforeEach(() => {
- loadFixtures('static/sketch_viewer.html.raw');
+ loadFixtures('static/sketch_viewer.html');
});
describe('with error message', () => {
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
index 93a942fe8d4..4ac15ca5aa2 100644
--- a/spec/javascripts/blob/viewer/index_spec.js
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -9,12 +9,12 @@ describe('Blob viewer', () => {
let blob;
let mock;
- preloadFixtures('snippets/show.html.raw');
+ preloadFixtures('snippets/show.html');
beforeEach(() => {
mock = new MockAdapter(axios);
- loadFixtures('snippets/show.html.raw');
+ loadFixtures('snippets/show.html');
$('#modal-upload-blob').remove();
blob = new BlobViewer();
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index e1017130bed..13b708a03d5 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -7,8 +7,8 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import eventHub from '~/boards/eventhub';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/list';
import boardsStore from '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
diff --git a/spec/javascripts/boards/board_list_common_spec.js b/spec/javascripts/boards/board_list_common_spec.js
new file mode 100644
index 00000000000..cb337e4cc83
--- /dev/null
+++ b/spec/javascripts/boards/board_list_common_spec.js
@@ -0,0 +1,58 @@
+/* global List */
+/* global ListIssue */
+
+import MockAdapter from 'axios-mock-adapter';
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import Sortable from 'sortablejs';
+import BoardList from '~/boards/components/board_list.vue';
+
+import '~/boards/models/issue';
+import '~/boards/models/list';
+import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
+import boardsStore from '~/boards/stores/boards_store';
+
+window.Sortable = Sortable;
+
+export default function createComponent({ done, listIssueProps = {}, componentProps = {} }) {
+ const el = document.createElement('div');
+
+ document.body.appendChild(el);
+ const mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+ gl.boardService = mockBoardService();
+ boardsStore.create();
+
+ const BoardListComp = Vue.extend(BoardList);
+ const list = new List(listObj);
+ const issue = new ListIssue({
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [],
+ assignees: [],
+ ...listIssueProps,
+ });
+ list.issuesSize = 1;
+ list.issues.push(issue);
+
+ const component = new BoardListComp({
+ el,
+ propsData: {
+ disabled: false,
+ list,
+ issues: list.issues,
+ loading: false,
+ issueLinkBase: '/issues',
+ rootPath: '/',
+ ...componentProps,
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ done();
+ });
+
+ return { component, mock };
+}
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index 2642c8b1bdb..9c9b435d7fd 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -1,60 +1,13 @@
-/* global List */
-/* global ListIssue */
-
import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import Sortable from 'sortablejs';
-import BoardList from '~/boards/components/board_list.vue';
import eventHub from '~/boards/eventhub';
-import '~/boards/models/issue';
-import '~/boards/models/list';
-import boardsStore from '~/boards/stores/boards_store';
-import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
-
-window.Sortable = Sortable;
+import createComponent from './board_list_common_spec';
describe('Board list component', () => {
let mock;
let component;
beforeEach(done => {
- const el = document.createElement('div');
-
- document.body.appendChild(el);
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- gl.boardService = mockBoardService();
- boardsStore.create();
-
- const BoardListComp = Vue.extend(BoardList);
- const list = new List(listObj);
- const issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [],
- assignees: [],
- });
- list.issuesSize = 1;
- list.issues.push(issue);
-
- component = new BoardListComp({
- el,
- propsData: {
- disabled: false,
- list,
- issues: list.issues,
- loading: false,
- issueLinkBase: '/issues',
- rootPath: '/',
- },
- }).$mount();
-
- Vue.nextTick(() => {
- done();
- });
+ ({ mock, component } = createComponent({ done }));
});
afterEach(() => {
@@ -195,7 +148,7 @@ describe('Board list component', () => {
component.list.loadingMore = true;
Vue.nextTick(() => {
- expect(component.$el.querySelector('.board-list-count .fa-spinner')).not.toBeNull();
+ expect(component.$el.querySelector('.board-list-count .spinner')).not.toBeNull();
done();
});
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 22f192bc7f3..e81115e10c9 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -6,12 +6,13 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
import boardsStore from '~/boards/stores/boards_store';
+import eventHub from '~/boards/eventhub';
import { listObj, listObjDuplicate, boardsMockInterceptor, mockBoardService } from './mock_data';
describe('Store', () => {
@@ -44,6 +45,48 @@ describe('Store', () => {
expect(boardsStore.state.lists.length).toBe(0);
});
+ describe('addList', () => {
+ it('sorts by position', () => {
+ boardsStore.addList({ position: 2 });
+ boardsStore.addList({ position: 1 });
+
+ expect(boardsStore.state.lists[0].position).toBe(1);
+ });
+ });
+
+ describe('toggleFilter', () => {
+ const dummyFilter = 'x=42';
+ let updateTokensSpy;
+
+ beforeEach(() => {
+ updateTokensSpy = jasmine.createSpy('updateTokens');
+ eventHub.$once('updateTokens', updateTokensSpy);
+
+ // prevent using window.history
+ spyOn(boardsStore, 'updateFiltersUrl').and.callFake(() => {});
+ });
+
+ it('adds the filter if it is not present', () => {
+ boardsStore.filter.path = 'something';
+
+ boardsStore.toggleFilter(dummyFilter);
+
+ expect(boardsStore.filter.path).toEqual(`something&${dummyFilter}`);
+ expect(updateTokensSpy).toHaveBeenCalled();
+ expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
+ });
+
+ it('removes the filter if it is present', () => {
+ boardsStore.filter.path = `something&${dummyFilter}`;
+
+ boardsStore.toggleFilter(dummyFilter);
+
+ expect(boardsStore.filter.path).toEqual('something');
+ expect(updateTokensSpy).toHaveBeenCalled();
+ expect(boardsStore.updateFiltersUrl).toHaveBeenCalled();
+ });
+ });
+
describe('lists', () => {
it('creates new list without persisting to DB', () => {
boardsStore.addList(listObj);
@@ -268,4 +311,48 @@ describe('Store', () => {
});
});
});
+
+ describe('setListDetail', () => {
+ it('sets the list detail', () => {
+ boardsStore.detail.list = 'not a list';
+
+ const dummyValue = 'new list';
+ boardsStore.setListDetail(dummyValue);
+
+ expect(boardsStore.detail.list).toEqual(dummyValue);
+ });
+ });
+
+ describe('clearDetailIssue', () => {
+ it('resets issue details', () => {
+ boardsStore.detail.issue = 'something';
+
+ boardsStore.clearDetailIssue();
+
+ expect(boardsStore.detail.issue).toEqual({});
+ });
+ });
+
+ describe('setIssueDetail', () => {
+ it('sets issue details', () => {
+ boardsStore.detail.issue = 'some details';
+
+ const dummyValue = 'new details';
+ boardsStore.setIssueDetail(dummyValue);
+
+ expect(boardsStore.detail.issue).toEqual(dummyValue);
+ });
+ });
+
+ describe('startMoving', () => {
+ it('stores list and issue', () => {
+ const dummyIssue = 'some issue';
+ const dummyList = 'some list';
+
+ boardsStore.startMoving(dummyList, dummyIssue);
+
+ expect(boardsStore.moving.issue).toEqual(dummyIssue);
+ expect(boardsStore.moving.list).toEqual(dummyList);
+ });
+ });
});
diff --git a/spec/javascripts/boards/components/board_spec.js b/spec/javascripts/boards/components/board_spec.js
index dee7841c088..d08ee41802b 100644
--- a/spec/javascripts/boards/components/board_spec.js
+++ b/spec/javascripts/boards/components/board_spec.js
@@ -9,7 +9,7 @@ describe('Board component', () => {
let el;
beforeEach(done => {
- loadFixtures('boards/show.html.raw');
+ loadFixtures('boards/show.html');
el = document.createElement('div');
document.body.appendChild(el);
@@ -103,4 +103,18 @@ describe('Board component', () => {
})
.catch(done.fail);
});
+
+ it('does render add issue button', () => {
+ expect(vm.$el.querySelector('.issue-count-badge-add-button')).not.toBeNull();
+ });
+
+ it('does not render add issue button when list type is blank', done => {
+ vm.list.type = 'blank';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.issue-count-badge-add-button')).toBeNull();
+
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js b/spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js
new file mode 100644
index 00000000000..c62c5b9962d
--- /dev/null
+++ b/spec/javascripts/boards/components/issue_card_inner_scoped_label_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import IssueCardInnerScopedLabel from '~/boards/components/issue_card_inner_scoped_label.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+describe('IssueCardInnerScopedLabel Component', () => {
+ let vm;
+ const Component = Vue.extend(IssueCardInnerScopedLabel);
+ const props = {
+ label: { title: 'Foo::Bar', description: 'Some Random Description' },
+ labelStyle: { background: 'white', color: 'black' },
+ scopedLabelsDocumentationLink: '/docs-link',
+ };
+ const createComponent = () => mountComponent(Component, { ...props });
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render label title', () => {
+ expect(vm.$el.querySelector('.color-label').textContent.trim()).toEqual('Foo::Bar');
+ });
+
+ it('should render question mark symbol', () => {
+ expect(vm.$el.querySelector('.fa-question-circle')).not.toBeNull();
+ });
+
+ it('should render label style provided', () => {
+ const node = vm.$el.querySelector('.color-label');
+
+ expect(node.style.background).toEqual(props.labelStyle.background);
+ expect(node.style.color).toEqual(props.labelStyle.color);
+ });
+
+ it('should render the docs link', () => {
+ expect(vm.$el.querySelector('a.scoped-label').href).toContain(
+ props.scopedLabelsDocumentationLink,
+ );
+ });
+});
diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js
index 054cf8c5b7d..68e26b68f04 100644
--- a/spec/javascripts/boards/components/issue_due_date_spec.js
+++ b/spec/javascripts/boards/components/issue_due_date_spec.js
@@ -43,7 +43,7 @@ describe('Issue Due Date component', () => {
date.setDate(date.getDate() + 5);
vm = createComponent(date);
- expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true));
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd'));
});
it('should render month and day for other dates', () => {
@@ -53,7 +53,7 @@ describe('Issue Due Date component', () => {
const isDueInCurrentYear = today.getFullYear() === date.getFullYear();
const format = isDueInCurrentYear ? 'mmm d' : 'mmm d, yyyy';
- expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, format, true));
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, format));
});
it('should contain the correct `.text-danger` css class for overdue issue', () => {
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 6eda5047dd0..8a20911cc66 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -4,8 +4,8 @@
import Vue from 'vue';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
@@ -285,4 +285,10 @@ describe('Issue card component', () => {
.catch(done.fail);
});
});
+
+ describe('weights', () => {
+ it('not shows weight component', () => {
+ expect(component.$el.querySelector('.board-card-weight')).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index 54fb0e8228b..bb7abe52eae 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -1,8 +1,8 @@
/* global ListIssue */
import Vue from 'vue';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
@@ -178,6 +178,7 @@ describe('Issue model', () => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
expect(data.issue.assignee_ids).toEqual([1]);
done();
+ return Promise.resolve();
});
issue.update('url');
@@ -187,6 +188,7 @@ describe('Issue model', () => {
spyOn(Vue.http, 'patch').and.callFake((url, data) => {
expect(data.issue.assignee_ids).toEqual([0]);
done();
+ return Promise.resolve();
});
issue.removeAllAssignees();
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 0d462a6f872..15c9ff6dfb4 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -4,8 +4,8 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
-import '~/vue_shared/models/label';
-import '~/vue_shared/models/assignee';
+import '~/boards/models/label';
+import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import '~/boards/services/board_service';
@@ -45,6 +45,7 @@ describe('List model', () => {
id: _.random(10000),
title: 'test',
color: 'red',
+ text_color: 'white',
},
});
list.save();
@@ -53,6 +54,8 @@ describe('List model', () => {
expect(list.id).toBe(listObj.id);
expect(list.type).toBe('label');
expect(list.position).toBe(0);
+ expect(list.label.color).toBe('red');
+ expect(list.label.textColor).toBe('white');
done();
}, 0);
});
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 14fff9223f4..9854cf49e97 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -16,6 +16,7 @@ export const listObj = {
title: 'Testing',
color: 'red',
description: 'testing;',
+ textColor: 'white',
},
};
@@ -46,7 +47,7 @@ export const BoardsMockData = {
},
],
},
- '/test/issue-boards/milestones.json': [
+ '/test/issue-boards/-/milestones.json': [
{
id: 1,
title: 'test',
@@ -57,10 +58,10 @@ export const BoardsMockData = {
'/test/-/boards/1/lists': listObj,
},
PUT: {
- '/test/issue-boards/board/1/lists{/id}': {},
+ '/test/issue-boards/-/board/1/lists{/id}': {},
},
DELETE: {
- '/test/issue-boards/board/1/lists{/id}': {},
+ '/test/issue-boards/-/board/1/lists{/id}': {},
},
};
@@ -70,7 +71,7 @@ export const boardsMockInterceptor = config => {
};
export const mockBoardService = (opts = {}) => {
- const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/boards.json';
+ const boardsEndpoint = opts.boardsEndpoint || '/test/issue-boards/-/boards.json';
const listsEndpoint = opts.listsEndpoint || '/test/-/boards/1/lists';
const bulkUpdatePath = opts.bulkUpdatePath || '';
const boardId = opts.boardId || '1';
diff --git a/spec/javascripts/bootstrap_linked_tabs_spec.js b/spec/javascripts/bootstrap_linked_tabs_spec.js
index c3e3d78ff63..1d21637ceae 100644
--- a/spec/javascripts/bootstrap_linked_tabs_spec.js
+++ b/spec/javascripts/bootstrap_linked_tabs_spec.js
@@ -1,10 +1,10 @@
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
describe('Linked Tabs', () => {
- preloadFixtures('static/linked_tabs.html.raw');
+ preloadFixtures('static/linked_tabs.html');
beforeEach(() => {
- loadFixtures('static/linked_tabs.html.raw');
+ loadFixtures('static/linked_tabs.html');
});
describe('when is initialized', () => {
diff --git a/spec/javascripts/breakpoints_spec.js b/spec/javascripts/breakpoints_spec.js
index 5ee777fee3f..fc0d9eb907a 100644
--- a/spec/javascripts/breakpoints_spec.js
+++ b/spec/javascripts/breakpoints_spec.js
@@ -10,4 +10,18 @@ describe('breakpoints', () => {
expect(bp.getBreakpointSize()).toBe(key);
});
});
+
+ describe('isDesktop', () => {
+ it('returns true when screen size is medium', () => {
+ spyOn(bp, 'windowWidth').and.returnValue(breakpoints.md + 10);
+
+ expect(bp.isDesktop()).toBe(true);
+ });
+
+ it('returns false when screen size is small', () => {
+ spyOn(bp, 'windowWidth').and.returnValue(breakpoints.sm + 10);
+
+ expect(bp.isDesktop()).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
index 1fc0e206d5e..e6a969bd855 100644
--- a/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ajax_variable_list_spec.js
@@ -7,8 +7,8 @@ const VARIABLE_PATCH_ENDPOINT = 'http://test.host/frontend-fixtures/builds-proje
const HIDE_CLASS = 'hide';
describe('AjaxFormVariableList', () => {
- preloadFixtures('projects/ci_cd_settings.html.raw');
- preloadFixtures('projects/ci_cd_settings_with_variables.html.raw');
+ preloadFixtures('projects/ci_cd_settings.html');
+ preloadFixtures('projects/ci_cd_settings_with_variables.html');
let container;
let saveButton;
@@ -18,7 +18,7 @@ describe('AjaxFormVariableList', () => {
let ajaxVariableList;
beforeEach(() => {
- loadFixtures('projects/ci_cd_settings.html.raw');
+ loadFixtures('projects/ci_cd_settings.html');
container = document.querySelector('.js-ci-variable-list-section');
mock = new MockAdapter(axios);
@@ -32,6 +32,7 @@ describe('AjaxFormVariableList', () => {
saveButton,
errorBox,
saveEndpoint: container.dataset.saveEndpoint,
+ maskableRegex: container.dataset.maskableRegex,
});
spyOn(ajaxVariableList, 'updateRowsWithPersistedVariables').and.callThrough();
@@ -113,7 +114,7 @@ describe('AjaxFormVariableList', () => {
it('hides secret values', done => {
mock.onPatch(VARIABLE_PATCH_ENDPOINT).reply(200, {});
- const row = container.querySelector('.js-row:first-child');
+ const row = container.querySelector('.js-row');
const valueInput = row.querySelector('.js-ci-variable-input-value');
const valuePlaceholder = row.querySelector('.js-secret-value-placeholder');
@@ -168,7 +169,7 @@ describe('AjaxFormVariableList', () => {
describe('updateRowsWithPersistedVariables', () => {
beforeEach(() => {
- loadFixtures('projects/ci_cd_settings_with_variables.html.raw');
+ loadFixtures('projects/ci_cd_settings_with_variables.html');
container = document.querySelector('.js-ci-variable-list-section');
const ajaxVariableListEl = document.querySelector('.js-ci-variable-list-section');
@@ -220,4 +221,11 @@ describe('AjaxFormVariableList', () => {
expect(row.dataset.isPersisted).toEqual('true');
});
});
+
+ describe('maskableRegex', () => {
+ it('takes in the regex provided by the data attribute', () => {
+ expect(container.dataset.maskableRegex).toBe('^[a-zA-Z0-9_+=/-]{8,}$');
+ expect(ajaxVariableList.maskableRegex).toBe(container.dataset.maskableRegex);
+ });
+ });
});
diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
index bef59b86d0c..064113e879a 100644
--- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
@@ -5,9 +5,9 @@ import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
const HIDE_CLASS = 'hide';
describe('VariableList', () => {
- preloadFixtures('pipeline_schedules/edit.html.raw');
- preloadFixtures('pipeline_schedules/edit_with_variables.html.raw');
- preloadFixtures('projects/ci_cd_settings.html.raw');
+ preloadFixtures('pipeline_schedules/edit.html');
+ preloadFixtures('pipeline_schedules/edit_with_variables.html');
+ preloadFixtures('projects/ci_cd_settings.html');
let $wrapper;
let variableList;
@@ -15,7 +15,7 @@ describe('VariableList', () => {
describe('with only key/value inputs', () => {
describe('with no variables', () => {
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit.html.raw');
+ loadFixtures('pipeline_schedules/edit.html');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -82,7 +82,7 @@ describe('VariableList', () => {
describe('with persisted variables', () => {
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit_with_variables.html.raw');
+ loadFixtures('pipeline_schedules/edit_with_variables.html');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -115,7 +115,7 @@ describe('VariableList', () => {
describe('with all inputs(key, value, protected)', () => {
beforeEach(() => {
- loadFixtures('projects/ci_cd_settings.html.raw');
+ loadFixtures('projects/ci_cd_settings.html');
$wrapper = $('.js-ci-variable-list-section');
$wrapper.find('.js-ci-variable-input-protected').attr('data-default', 'false');
@@ -127,29 +127,93 @@ describe('VariableList', () => {
variableList.init();
});
- it('should add another row when editing the last rows protected checkbox', done => {
+ it('should not add another row when editing the last rows protected checkbox', done => {
const $row = $wrapper.find('.js-row:last-child');
$row.find('.ci-variable-protected-item .js-project-feature-toggle').click();
getSetTimeoutPromise()
.then(() => {
- expect($wrapper.find('.js-row').length).toBe(2);
+ expect($wrapper.find('.js-row').length).toBe(1);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
- // Check for the correct default in the new row
- const $protectedInput = $wrapper
- .find('.js-row:last-child')
- .find('.js-ci-variable-input-protected');
+ it('should not add another row when editing the last rows masked checkbox', done => {
+ const $row = $wrapper.find('.js-row:last-child');
+ $row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
- expect($protectedInput.val()).toBe('false');
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find('.js-row').length).toBe(1);
})
.then(done)
.catch(done.fail);
});
+
+ describe('validateMaskability', () => {
+ let $row;
+
+ const maskingErrorElement = '.js-row:last-child .masking-validation-error';
+
+ beforeEach(() => {
+ $row = $wrapper.find('.js-row:last-child');
+ $row.find('.ci-variable-masked-item .js-project-feature-toggle').click();
+ });
+
+ it('has a regex provided via a data attribute', () => {
+ expect($wrapper.attr('data-maskable-regex')).toBe('^[a-zA-Z0-9_+=/-]{8,}$');
+ });
+
+ it('allows values that are 8 characters long', done => {
+ $row.find('.js-ci-variable-input-value').val('looooong');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('rejects values that are shorter than 8 characters', done => {
+ $row.find('.js-ci-variable-input-value').val('short');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find(maskingErrorElement)).toBeVisible();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('allows values with base 64 characters', done => {
+ $row.find('.js-ci-variable-input-value').val('abcABC123_+=/-');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find(maskingErrorElement)).toHaveClass('hide');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('rejects values with other special characters', done => {
+ $row.find('.js-ci-variable-input-value').val('1234567$');
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect($wrapper.find(maskingErrorElement)).toBeVisible();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
describe('toggleEnableRow method', () => {
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit_with_variables.html.raw');
+ loadFixtures('pipeline_schedules/edit_with_variables.html');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
@@ -198,7 +262,7 @@ describe('VariableList', () => {
describe('hideValues', () => {
beforeEach(() => {
- loadFixtures('projects/ci_cd_settings.html.raw');
+ loadFixtures('projects/ci_cd_settings.html');
$wrapper = $('.js-ci-variable-list-section');
variableList = new VariableList({
diff --git a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
index 997d0d54d79..4982b68fa81 100644
--- a/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
@@ -2,12 +2,12 @@ import $ from 'jquery';
import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
describe('NativeFormVariableList', () => {
- preloadFixtures('pipeline_schedules/edit.html.raw');
+ preloadFixtures('pipeline_schedules/edit.html');
let $wrapper;
beforeEach(() => {
- loadFixtures('pipeline_schedules/edit.html.raw');
+ loadFixtures('pipeline_schedules/edit.html');
$wrapper = $('.js-ci-variable-list-section');
setupNativeFormVariableList({
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
deleted file mode 100644
index 7928feeadfa..00000000000
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ /dev/null
@@ -1,268 +0,0 @@
-import Clusters from '~/clusters/clusters_bundle';
-import { REQUEST_SUBMITTED, REQUEST_FAILURE, APPLICATION_STATUS } from '~/clusters/constants';
-import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
-
-describe('Clusters', () => {
- let cluster;
- preloadFixtures('clusters/show_cluster.html.raw');
-
- beforeEach(() => {
- loadFixtures('clusters/show_cluster.html.raw');
- cluster = new Clusters();
- });
-
- afterEach(() => {
- cluster.destroy();
- });
-
- describe('toggle', () => {
- it('should update the button and the input field on click', done => {
- const toggleButton = document.querySelector(
- '.js-cluster-enable-toggle-area .js-project-feature-toggle',
- );
- const toggleInput = document.querySelector(
- '.js-cluster-enable-toggle-area .js-project-feature-toggle-input',
- );
-
- toggleButton.click();
-
- getSetTimeoutPromise()
- .then(() => {
- expect(toggleButton.classList).not.toContain('is-checked');
-
- expect(toggleInput.getAttribute('value')).toEqual('false');
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('showToken', () => {
- it('should update token field type', () => {
- cluster.showTokenButton.click();
-
- expect(cluster.tokenField.getAttribute('type')).toEqual('text');
-
- cluster.showTokenButton.click();
-
- expect(cluster.tokenField.getAttribute('type')).toEqual('password');
- });
-
- it('should update show token button text', () => {
- cluster.showTokenButton.click();
-
- expect(cluster.showTokenButton.textContent).toEqual('Hide');
-
- cluster.showTokenButton.click();
-
- expect(cluster.showTokenButton.textContent).toEqual('Show');
- });
- });
-
- describe('checkForNewInstalls', () => {
- const INITIAL_APP_MAP = {
- helm: { status: null, title: 'Helm Tiller' },
- ingress: { status: null, title: 'Ingress' },
- runner: { status: null, title: 'GitLab Runner' },
- };
-
- it('does not show alert when things transition from initial null state to something', () => {
- cluster.checkForNewInstalls(INITIAL_APP_MAP, {
- ...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Helm Tiller' },
- });
-
- const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
-
- expect(flashMessage).toBeNull();
- });
-
- it('shows an alert when something gets newly installed', () => {
- cluster.checkForNewInstalls(
- {
- ...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
- },
- {
- ...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
- },
- );
-
- const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
-
- expect(flashMessage).not.toBeNull();
- expect(flashMessage.textContent.trim()).toEqual(
- 'Helm Tiller was successfully installed on your Kubernetes cluster',
- );
- });
-
- it('shows an alert when multiple things gets newly installed', () => {
- cluster.checkForNewInstalls(
- {
- ...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLING, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLABLE, title: 'Ingress' },
- },
- {
- ...INITIAL_APP_MAP,
- helm: { status: APPLICATION_STATUS.INSTALLED, title: 'Helm Tiller' },
- ingress: { status: APPLICATION_STATUS.INSTALLED, title: 'Ingress' },
- },
- );
-
- const flashMessage = document.querySelector('.js-cluster-application-notice .flash-text');
-
- expect(flashMessage).not.toBeNull();
- expect(flashMessage.textContent.trim()).toEqual(
- 'Helm Tiller, Ingress was successfully installed on your Kubernetes cluster',
- );
- });
- });
-
- describe('updateContainer', () => {
- describe('when creating cluster', () => {
- it('should show the creating container', () => {
- cluster.updateContainer(null, 'creating');
-
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy();
-
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
- });
-
- it('should continue to show `creating` banner with subsequent updates of the same status', () => {
- cluster.updateContainer('creating', 'creating');
-
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeFalsy();
-
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
- });
- });
-
- describe('when cluster is created', () => {
- it('should show the success container and fresh the page', () => {
- cluster.updateContainer(null, 'created');
-
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.successContainer.classList.contains('hidden')).toBeFalsy();
-
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
- });
-
- it('should not show a banner when status is already `created`', () => {
- cluster.updateContainer('created', 'created');
-
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.errorContainer.classList.contains('hidden')).toBeTruthy();
- });
- });
-
- describe('when cluster has error', () => {
- it('should show the error container', () => {
- cluster.updateContainer(null, 'errored', 'this is an error');
-
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy();
-
- expect(cluster.errorReasonContainer.textContent).toContain('this is an error');
- });
-
- it('should show `error` banner when previously `creating`', () => {
- cluster.updateContainer('creating', 'errored');
-
- expect(cluster.creatingContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.successContainer.classList.contains('hidden')).toBeTruthy();
-
- expect(cluster.errorContainer.classList.contains('hidden')).toBeFalsy();
- });
- });
- });
-
- describe('installApplication', () => {
- it('tries to install helm', () => {
- spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
-
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
-
- cluster.installApplication({ id: 'helm' });
-
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
- });
-
- it('tries to install ingress', () => {
- spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
-
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
-
- cluster.installApplication({ id: 'ingress' });
-
- expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
- });
-
- it('tries to install runner', () => {
- spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
-
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
-
- cluster.installApplication({ id: 'runner' });
-
- expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
- });
-
- it('tries to install jupyter', () => {
- spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
-
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
- cluster.installApplication({
- id: 'jupyter',
- params: { hostname: cluster.store.state.applications.jupyter.hostname },
- });
-
- expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', {
- hostname: cluster.store.state.applications.jupyter.hostname,
- });
- });
-
- it('sets error request status when the request fails', done => {
- spyOn(cluster.service, 'installApplication').and.returnValue(
- Promise.reject(new Error('STUBBED ERROR')),
- );
-
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
-
- cluster.installApplication({ id: 'helm' });
-
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_SUBMITTED);
- expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
- expect(cluster.service.installApplication).toHaveBeenCalled();
-
- getSetTimeoutPromise()
- .then(() => {
- expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_FAILURE);
- expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index dc5737558c0..bb90e53e525 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -6,7 +6,7 @@ import Sidebar from '~/right_sidebar';
import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Issuable right sidebar collapsed todo toggle', () => {
- const fixtureName = 'issues/open-issue.html.raw';
+ const fixtureName = 'issues/open-issue.html';
const jsonFixtureName = 'todos/todos.json';
let mock;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index 04c8ab44405..fec01b1f0a3 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -87,7 +87,7 @@ describe('Pipelines table in Commits and Merge requests', function() {
};
vm.$nextTick(() => {
- vm.$el.querySelector('.js-next-button a').click();
+ vm.$el.querySelector('.js-next-button .page-link').click();
expect(vm.updateContent).toHaveBeenCalledWith({ page: '2' });
done();
diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/javascripts/create_item_dropdown_spec.js
index 9cf72d7c55b..a814952faab 100644
--- a/spec/javascripts/create_item_dropdown_spec.js
+++ b/spec/javascripts/create_item_dropdown_spec.js
@@ -20,7 +20,7 @@ const DROPDOWN_ITEM_DATA = [
];
describe('CreateItemDropdown', () => {
- preloadFixtures('static/create_item_dropdown.html.raw');
+ preloadFixtures('static/create_item_dropdown.html');
let $wrapperEl;
let createItemDropdown;
@@ -44,7 +44,7 @@ describe('CreateItemDropdown', () => {
}
beforeEach(() => {
- loadFixtures('static/create_item_dropdown.html.raw');
+ loadFixtures('static/create_item_dropdown.html');
$wrapperEl = $('.js-create-item-dropdown-fixture-root');
});
diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js
index 5abdfe695d0..1aabf3c2132 100644
--- a/spec/javascripts/diffs/components/app_spec.js
+++ b/spec/javascripts/diffs/components/app_spec.js
@@ -1,15 +1,24 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
import NoChanges from '~/diffs/components/no_changes.vue';
import DiffFile from '~/diffs/components/diff_file.vue';
+import Mousetrap from 'mousetrap';
+import CompareVersions from '~/diffs/components/compare_versions.vue';
+import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
+import CommitWidget from '~/diffs/components/commit_widget.vue';
+import TreeList from '~/diffs/components/tree_list.vue';
import createDiffsStore from '../create_diffs_store';
+import diffsMockData from '../mock_data/merge_request_diffs';
+
+const mergeRequestDiff = { version_index: 1 };
describe('diffs/components/app', () => {
const oldMrTabs = window.mrTabs;
let store;
- let vm;
+ let wrapper;
function createComponent(props = {}, extendStore = () => {}) {
const localVue = createLocalVue();
@@ -21,7 +30,7 @@ describe('diffs/components/app', () => {
extendStore(store);
- vm = shallowMount(localVue.extend(App), {
+ wrapper = shallowMount(localVue.extend(App), {
localVue,
propsData: {
endpoint: `${TEST_HOST}/diff/endpoint`,
@@ -38,7 +47,6 @@ describe('diffs/components/app', () => {
// setup globals (needed for component to mount :/)
window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']);
window.mrTabs.expandViewContainer = jasmine.createSpy();
- window.location.hash = 'ABC_123';
});
afterEach(() => {
@@ -46,25 +54,85 @@ describe('diffs/components/app', () => {
window.mrTabs = oldMrTabs;
// reset component
- vm.destroy();
+ wrapper.destroy();
+ });
+
+ it('adds container-limiting classes when showFileTree is false with inline diffs', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.showTreeList = false;
+ state.diffs.isParallelView = false;
+ });
+
+ expect(wrapper.contains('.container-limited.limit-container-width')).toBe(true);
+ });
+
+ it('does not add container-limiting classes when showFileTree is false with inline diffs', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.showTreeList = true;
+ state.diffs.isParallelView = false;
+ });
+
+ expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false);
+ });
+
+ it('does not add container-limiting classes when isFluidLayout', () => {
+ createComponent({ isFluidLayout: true }, ({ state }) => {
+ state.diffs.isParallelView = false;
+ });
+
+ expect(wrapper.contains('.container-limited.limit-container-width')).toBe(false);
+ });
+
+ it('displays loading icon on loading', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.isLoading = true;
+ });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('displays diffs container when not loading', () => {
+ createComponent();
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(false);
+ expect(wrapper.contains('#diffs')).toBe(true);
});
it('does not show commit info', () => {
createComponent();
- expect(vm.contains('.blob-commit-info')).toBe(false);
+ expect(wrapper.contains('.blob-commit-info')).toBe(false);
});
- it('sets highlighted row if hash exists in location object', done => {
- createComponent({
- shouldShow: true,
+ describe('row highlighting', () => {
+ beforeEach(() => {
+ window.location.hash = 'ABC_123';
});
- // Component uses $nextTick so we wait until that has finished
- setTimeout(() => {
- expect(store.state.diffs.highlightedRow).toBe('ABC_123');
+ it('sets highlighted row if hash exists in location object', done => {
+ createComponent({
+ shouldShow: true,
+ });
- done();
+ // Component uses $nextTick so we wait until that has finished
+ setTimeout(() => {
+ expect(store.state.diffs.highlightedRow).toBe('ABC_123');
+
+ done();
+ });
+ });
+
+ it('marks current diff file based on currently highlighted row', done => {
+ createComponent({
+ shouldShow: true,
+ });
+
+ // Component uses $nextTick so we wait until that has finished
+ setTimeout(() => {
+ expect(store.state.diffs.currentDiffFileId).toBe('ABC');
+
+ done();
+ });
});
});
@@ -76,7 +144,7 @@ describe('diffs/components/app', () => {
it('sets initial width when no localStorage has been set', () => {
createComponent();
- expect(vm.vm.treeWidth).toEqual(320);
+ expect(wrapper.vm.treeWidth).toEqual(320);
});
it('sets initial width to localStorage size', () => {
@@ -84,13 +152,26 @@ describe('diffs/components/app', () => {
createComponent();
- expect(vm.vm.treeWidth).toEqual(200);
+ expect(wrapper.vm.treeWidth).toEqual(200);
});
it('sets width of tree list', () => {
createComponent();
- expect(vm.find('.js-diff-tree-list').element.style.width).toEqual('320px');
+ expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px');
+ });
+ });
+
+ it('marks current diff file based on currently highlighted row', done => {
+ createComponent({
+ shouldShow: true,
+ });
+
+ // Component uses $nextTick so we wait until that has finished
+ setTimeout(() => {
+ expect(store.state.diffs.currentDiffFileId).toBe('ABC');
+
+ done();
});
});
@@ -98,27 +179,305 @@ describe('diffs/components/app', () => {
it('renders empty state when no diff files exist', () => {
createComponent();
- expect(vm.contains(NoChanges)).toBe(true);
+ expect(wrapper.contains(NoChanges)).toBe(true);
});
it('does not render empty state when diff files exist', () => {
- createComponent({}, () => {
- store.state.diffs.diffFiles.push({
+ createComponent({}, ({ state }) => {
+ state.diffs.diffFiles.push({
id: 1,
});
});
- expect(vm.contains(NoChanges)).toBe(false);
- expect(vm.findAll(DiffFile).length).toBe(1);
+ expect(wrapper.contains(NoChanges)).toBe(false);
+ expect(wrapper.findAll(DiffFile).length).toBe(1);
});
it('does not render empty state when versions match', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.startVersion = mergeRequestDiff;
+ state.diffs.mergeRequestDiff = mergeRequestDiff;
+ });
+
+ expect(wrapper.contains(NoChanges)).toBe(false);
+ });
+ });
+
+ describe('keyboard shortcut navigation', () => {
+ const mappings = {
+ '[': -1,
+ k: -1,
+ ']': +1,
+ j: +1,
+ };
+ let spy;
+
+ describe('visible app', () => {
+ beforeEach(() => {
+ spy = jasmine.createSpy('spy');
+
+ createComponent({
+ shouldShow: true,
+ });
+ wrapper.setMethods({
+ jumpToFile: spy,
+ });
+ });
+
+ it('calls `jumpToFile()` with correct parameter whenever pre-defined key is pressed', done => {
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ Object.keys(mappings).forEach(function(key) {
+ Mousetrap.trigger(key);
+
+ expect(spy.calls.mostRecent().args).toEqual([mappings[key]]);
+ });
+
+ expect(spy.calls.count()).toEqual(Object.keys(mappings).length);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not call `jumpToFile()` when unknown key is pressed', done => {
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ Mousetrap.trigger('d');
+
+ expect(spy).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('hideen app', () => {
+ beforeEach(() => {
+ spy = jasmine.createSpy('spy');
+
+ createComponent({
+ shouldShow: false,
+ });
+ wrapper.setMethods({
+ jumpToFile: spy,
+ });
+ });
+
+ it('stops calling `jumpToFile()` when application is hidden', done => {
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ Object.keys(mappings).forEach(function(key) {
+ Mousetrap.trigger(key);
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('jumpToFile', () => {
+ let spy;
+
+ beforeEach(() => {
+ spy = jasmine.createSpy();
+
createComponent({}, () => {
- store.state.diffs.startVersion = { version_index: 1 };
- store.state.diffs.mergeRequestDiff = { version_index: 1 };
+ store.state.diffs.diffFiles = [
+ { file_hash: '111', file_path: '111.js' },
+ { file_hash: '222', file_path: '222.js' },
+ { file_hash: '333', file_path: '333.js' },
+ ];
+ });
+
+ wrapper.setMethods({
+ scrollToFile: spy,
});
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('jumps to next and previous files in the list', done => {
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ wrapper.vm.jumpToFile(+1);
+
+ expect(spy.calls.mostRecent().args).toEqual(['222.js']);
+ store.state.diffs.currentDiffFileId = '222';
+ wrapper.vm.jumpToFile(+1);
+
+ expect(spy.calls.mostRecent().args).toEqual(['333.js']);
+ store.state.diffs.currentDiffFileId = '333';
+ wrapper.vm.jumpToFile(-1);
+
+ expect(spy.calls.mostRecent().args).toEqual(['222.js']);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not jump to previous file from the first one', done => {
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ store.state.diffs.currentDiffFileId = '333';
+
+ expect(wrapper.vm.currentDiffIndex).toEqual(2);
+
+ wrapper.vm.jumpToFile(+1);
+
+ expect(wrapper.vm.currentDiffIndex).toEqual(2);
+ expect(spy).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not jump to next file from the last one', done => {
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.vm.currentDiffIndex).toEqual(0);
+
+ wrapper.vm.jumpToFile(-1);
+
+ expect(wrapper.vm.currentDiffIndex).toEqual(0);
+ expect(spy).not.toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('diffs', () => {
+ it('should render compare versions component', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.mergeRequestDiffs = diffsMockData;
+ state.diffs.targetBranchName = 'target-branch';
+ state.diffs.mergeRequestDiff = mergeRequestDiff;
+ });
+
+ expect(wrapper.contains(CompareVersions)).toBe(true);
+ expect(wrapper.find(CompareVersions).props()).toEqual(
+ jasmine.objectContaining({
+ targetBranch: {
+ branchName: 'target-branch',
+ versionIndex: -1,
+ path: '',
+ },
+ mergeRequestDiffs: diffsMockData,
+ mergeRequestDiff,
+ }),
+ );
+ });
+
+ it('should render hidden files warning if render overflow warning is present', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.renderOverflowWarning = true;
+ state.diffs.realSize = '5';
+ state.diffs.plainDiffPath = 'plain diff path';
+ state.diffs.emailPatchPath = 'email patch path';
+ state.diffs.size = 1;
+ });
+
+ expect(wrapper.contains(HiddenFilesWarning)).toBe(true);
+ expect(wrapper.find(HiddenFilesWarning).props()).toEqual(
+ jasmine.objectContaining({
+ total: '5',
+ plainDiffPath: 'plain diff path',
+ emailPatchPath: 'email patch path',
+ visible: 1,
+ }),
+ );
+ });
+
+ it('should display commit widget if store has a commit', () => {
+ createComponent({}, () => {
+ store.state.diffs.commit = {
+ author: 'John Doe',
+ };
+ });
+
+ expect(wrapper.contains(CommitWidget)).toBe(true);
+ });
+
+ it('should display diff file if there are diff files', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.diffFiles.push({ sha: '123' });
+ });
+
+ expect(wrapper.contains(DiffFile)).toBe(true);
+ });
+
+ it('should render tree list', () => {
+ createComponent();
+
+ expect(wrapper.find(TreeList).exists()).toBe(true);
+ });
+ });
+
+ describe('hideTreeListIfJustOneFile', () => {
+ let toggleShowTreeList;
+
+ beforeEach(() => {
+ toggleShowTreeList = jasmine.createSpy('toggleShowTreeList');
+ });
+
+ afterEach(() => {
+ localStorage.removeItem('mr_tree_show');
+ });
+
+ it('calls toggleShowTreeList when only 1 file', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.diffFiles.push({ sha: '123' });
+ });
+
+ wrapper.setMethods({
+ toggleShowTreeList,
+ });
+
+ wrapper.vm.hideTreeListIfJustOneFile();
+
+ expect(toggleShowTreeList).toHaveBeenCalledWith(false);
+ });
+
+ it('does not call toggleShowTreeList when more than 1 file', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.diffFiles.push({ sha: '123' });
+ state.diffs.diffFiles.push({ sha: '124' });
+ });
+
+ wrapper.setMethods({
+ toggleShowTreeList,
+ });
+
+ wrapper.vm.hideTreeListIfJustOneFile();
+
+ expect(toggleShowTreeList).not.toHaveBeenCalled();
+ });
+
+ it('does not call toggleShowTreeList when localStorage is set', () => {
+ localStorage.setItem('mr_tree_show', 'true');
+
+ createComponent({}, ({ state }) => {
+ state.diffs.diffFiles.push({ sha: '123' });
+ });
+
+ wrapper.setMethods({
+ toggleShowTreeList,
+ });
+
+ wrapper.vm.hideTreeListIfJustOneFile();
- expect(vm.contains(NoChanges)).toBe(false);
+ expect(toggleShowTreeList).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/javascripts/diffs/components/changed_files_dropdown_spec.js b/spec/javascripts/diffs/components/changed_files_dropdown_spec.js
deleted file mode 100644
index 7237274eb43..00000000000
--- a/spec/javascripts/diffs/components/changed_files_dropdown_spec.js
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/commit_item_spec.js b/spec/javascripts/diffs/components/commit_item_spec.js
index 50e45f48af3..cfe0c4bad71 100644
--- a/spec/javascripts/diffs/components/commit_item_spec.js
+++ b/spec/javascripts/diffs/components/commit_item_spec.js
@@ -1,14 +1,14 @@
import Vue from 'vue';
import { TEST_HOST } from 'spec/test_constants';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { trimText } from 'spec/helpers/vue_component_helper';
+import { trimText } from 'spec/helpers/text_helper';
import { getTimeago } from '~/lib/utils/datetime_utility';
import CommitItem from '~/diffs/components/commit_item.vue';
import getDiffWithCommit from '../mock_data/diff_with_commit';
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
-const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=36`;
+const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
const TEST_SIGNATURE_HTML = '<a>Legit commit</a>';
const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
@@ -65,7 +65,7 @@ describe('diffs/components/commit_item', () => {
const imgElement = avatarElement.querySelector('img');
expect(avatarElement).toHaveAttr('href', commit.author.web_url);
- expect(imgElement).toHaveClass('s36');
+ expect(imgElement).toHaveClass('s40');
expect(imgElement).toHaveAttr('alt', commit.author.name);
expect(imgElement).toHaveAttr('src', commit.author.avatar_url);
});
diff --git a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js
index 53b9ac22fc0..8a3834d542f 100644
--- a/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js
+++ b/spec/javascripts/diffs/components/compare_versions_dropdown_spec.js
@@ -1,34 +1,161 @@
-// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
import { shallowMount, createLocalVue } from '@vue/test-utils';
import CompareVersionsDropdown from '~/diffs/components/compare_versions_dropdown.vue';
import diffsMockData from '../mock_data/merge_request_diffs';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+
+const localVue = createLocalVue();
+const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
+const startVersion = { version_index: 4 };
+const mergeRequestVersion = {
+ version_path: '123',
+};
+const baseVersionPath = '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37';
describe('CompareVersionsDropdown', () => {
let wrapper;
- const targetBranch = { branchName: 'tmp-wine-dev', versionIndex: -1 };
- const factory = (options = {}) => {
- const localVue = createLocalVue();
+ const findSelectedVersion = () => wrapper.find('.dropdown-menu-toggle');
+ const findVersionsListElements = () => wrapper.findAll('li');
+ const findLinkElement = index =>
+ findVersionsListElements()
+ .at(index)
+ .find('a');
+ const findLastLink = () => findLinkElement(findVersionsListElements().length - 1);
- wrapper = shallowMount(CompareVersionsDropdown, { localVue, ...options });
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(localVue.extend(CompareVersionsDropdown), {
+ localVue,
+ sync: false,
+ propsData: { ...props },
+ });
};
afterEach(() => {
wrapper.destroy();
});
- it('should render a correct base version link', () => {
- factory({
- propsData: {
- baseVersionPath: '/gnuwget/wget2/merge_requests/6/diffs?diff_id=37',
+ describe('selected version name', () => {
+ it('shows latest version when latest is selected', () => {
+ createComponent({
+ mergeRequestVersion,
+ startVersion,
+ otherVersions: diffsMockData,
+ });
+
+ expect(findSelectedVersion().text()).toBe('latest version');
+ });
+
+ it('shows target branch name for base branch', () => {
+ createComponent({
+ targetBranch,
+ });
+
+ expect(findSelectedVersion().text()).toBe('tmp-wine-dev');
+ });
+
+ it('shows correct version for non-base and non-latest branches', () => {
+ createComponent({
+ startVersion,
+ targetBranch,
+ });
+
+ expect(findSelectedVersion().text()).toBe(`version ${startVersion.version_index}`);
+ });
+ });
+
+ describe('target versions list', () => {
+ it('should have the same length as otherVersions if merge request version is present', () => {
+ createComponent({
+ mergeRequestVersion,
+ otherVersions: diffsMockData,
+ });
+
+ expect(findVersionsListElements().length).toEqual(diffsMockData.length);
+ });
+
+ it('should have an otherVersions length plus 1 if no merge request version is present', () => {
+ createComponent({
+ targetBranch,
+ otherVersions: diffsMockData,
+ });
+
+ expect(findVersionsListElements().length).toEqual(diffsMockData.length + 1);
+ });
+
+ it('should have base branch link as active on base branch', () => {
+ createComponent({
+ targetBranch,
+ otherVersions: diffsMockData,
+ });
+
+ expect(findLastLink().classes()).toContain('is-active');
+ });
+
+ it('should have correct branch link as active if start version present', () => {
+ createComponent({
+ targetBranch,
+ startVersion,
+ otherVersions: diffsMockData,
+ });
+
+ expect(findLinkElement(0).classes()).toContain('is-active');
+ });
+
+ it('should render a correct base version link', () => {
+ createComponent({
+ baseVersionPath,
otherVersions: diffsMockData.slice(1),
targetBranch,
- },
+ });
+
+ expect(findLastLink().attributes('href')).toEqual(baseVersionPath);
+ expect(findLastLink().text()).toContain('(base)');
+ });
+
+ it('should not render commits count if no showCommitsCount is passed', () => {
+ createComponent({
+ otherVersions: diffsMockData,
+ targetBranch,
+ });
+
+ const commitsCount = diffsMockData[0].commits_count;
+
+ expect(findLinkElement(0).text()).not.toContain(`${commitsCount} commit`);
+ });
+
+ it('should render correct commits count if showCommitsCount is passed', () => {
+ createComponent({
+ otherVersions: diffsMockData,
+ targetBranch,
+ showCommitCount: true,
+ });
+
+ const commitsCount = diffsMockData[0].commits_count;
+
+ expect(findLinkElement(0).text()).toContain(`${commitsCount} commit`);
+ });
+
+ it('should render correct commit sha', () => {
+ createComponent({
+ otherVersions: diffsMockData,
+ targetBranch,
+ });
+
+ const commitShaElement = findLinkElement(0).find('.commit-sha');
+
+ expect(commitShaElement.text()).toBe(diffsMockData[0].short_commit_sha);
});
- const links = wrapper.findAll('a');
- const lastLink = links.wrappers[links.length - 1];
+ it('should render correct time-ago ', () => {
+ createComponent({
+ otherVersions: diffsMockData,
+ targetBranch,
+ });
+
+ const timeAgoElement = findLinkElement(0).find(TimeAgo);
- expect(lastLink.attributes('href')).toEqual(wrapper.props('baseVersionPath'));
+ expect(timeAgoElement.exists()).toBe(true);
+ expect(timeAgoElement.props('time')).toBe(diffsMockData[0].created_at);
+ });
});
});
diff --git a/spec/javascripts/diffs/components/compare_versions_spec.js b/spec/javascripts/diffs/components/compare_versions_spec.js
index e886f962d2f..77f8352047c 100644
--- a/spec/javascripts/diffs/components/compare_versions_spec.js
+++ b/spec/javascripts/diffs/components/compare_versions_spec.js
@@ -66,6 +66,26 @@ describe('CompareVersions', () => {
expect(inlineBtn.innerHTML).toContain('Inline');
expect(parallelBtn.innerHTML).toContain('Side-by-side');
});
+
+ it('adds container-limiting classes when showFileTree is false with inline diffs', () => {
+ vm.isLimitedContainer = true;
+
+ vm.$nextTick(() => {
+ const limitedContainer = vm.$el.querySelector('.container-limited.limit-container-width');
+
+ expect(limitedContainer).not.toBeNull();
+ });
+ });
+
+ it('does not add container-limiting classes when showFileTree is false with inline diffs', () => {
+ vm.isLimitedContainer = false;
+
+ vm.$nextTick(() => {
+ const limitedContainer = vm.$el.querySelector('.container-limited.limit-container-width');
+
+ expect(limitedContainer).toBeNull();
+ });
+ });
});
describe('setInlineDiffViewType', () => {
diff --git a/spec/javascripts/diffs/components/diff_content_spec.js b/spec/javascripts/diffs/components/diff_content_spec.js
index a1bb51963d6..124bdeea45d 100644
--- a/spec/javascripts/diffs/components/diff_content_spec.js
+++ b/spec/javascripts/diffs/components/diff_content_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
-import { createStore } from '~/mr_notes/stores';
+import { createStore } from 'ee_else_ce/mr_notes/stores';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL, RED_BOX_IMAGE_URL } from 'spec/test_constants';
import '~/behaviors/markdown/render_gfm';
@@ -29,6 +29,10 @@ describe('DiffContent', () => {
});
});
+ afterEach(() => {
+ vm.$destroy();
+ });
+
describe('text based files', () => {
it('should render diff inline view', done => {
vm.$store.state.diffs.diffViewType = 'inline';
@@ -49,6 +53,16 @@ describe('DiffContent', () => {
done();
});
});
+
+ it('renders rendering more lines loading icon', done => {
+ vm.diffFile.renderingLines = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+ done();
+ });
+ });
});
describe('empty files', () => {
diff --git a/spec/javascripts/diffs/components/diff_discussions_spec.js b/spec/javascripts/diffs/components/diff_discussions_spec.js
index 0bc9da5ad0f..f7f0ab83c21 100644
--- a/spec/javascripts/diffs/components/diff_discussions_spec.js
+++ b/spec/javascripts/diffs/components/diff_discussions_spec.js
@@ -1,90 +1,103 @@
-import Vue from 'vue';
+import { mount, createLocalVue } from '@vue/test-utils';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import NoteableDiscussion from '~/notes/components/noteable_discussion.vue';
+import DiscussionNotes from '~/notes/components/discussion_notes.vue';
+import Icon from '~/vue_shared/components/icon.vue';
import { createStore } from '~/mr_notes/stores';
-import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import '~/behaviors/markdown/render_gfm';
import discussionsMockData from '../mock_data/diff_discussions';
+const localVue = createLocalVue();
+
describe('DiffDiscussions', () => {
- let vm;
+ let store;
+ let wrapper;
const getDiscussionsMockData = () => [Object.assign({}, discussionsMockData)];
- function createComponent(props = {}) {
- const store = createStore();
-
- vm = createComponentWithStore(Vue.extend(DiffDiscussions), store, {
- discussions: getDiscussionsMockData(),
- ...props,
- }).$mount();
- }
+ const createComponent = props => {
+ store = createStore();
+ wrapper = mount(localVue.extend(DiffDiscussions), {
+ store,
+ propsData: {
+ discussions: getDiscussionsMockData(),
+ ...props,
+ },
+ localVue,
+ sync: false,
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('template', () => {
it('should have notes list', () => {
createComponent();
- expect(vm.$el.querySelectorAll('.discussion .note.timeline-entry').length).toEqual(5);
+ expect(wrapper.find(NoteableDiscussion).exists()).toBe(true);
+ expect(wrapper.find(DiscussionNotes).exists()).toBe(true);
+ expect(wrapper.find(DiscussionNotes).findAll(TimelineEntryItem).length).toBe(
+ discussionsMockData.notes.length,
+ );
});
});
describe('image commenting', () => {
+ const findDiffNotesToggle = () => wrapper.find('.js-diff-notes-toggle');
+
it('renders collapsible discussion button', () => {
createComponent({ shouldCollapseDiscussions: true });
+ const diffNotesToggle = findDiffNotesToggle();
- expect(vm.$el.querySelector('.js-diff-notes-toggle')).not.toBe(null);
- expect(vm.$el.querySelector('.js-diff-notes-toggle svg')).not.toBe(null);
- expect(vm.$el.querySelector('.js-diff-notes-toggle').classList).toContain(
- 'diff-notes-collapse',
- );
+ expect(diffNotesToggle.exists()).toBe(true);
+ expect(diffNotesToggle.find(Icon).exists()).toBe(true);
+ expect(diffNotesToggle.classes('diff-notes-collapse')).toBe(true);
});
it('dispatches toggleDiscussion when clicking collapse button', () => {
createComponent({ shouldCollapseDiscussions: true });
+ spyOn(wrapper.vm.$store, 'dispatch').and.stub();
+ const diffNotesToggle = findDiffNotesToggle();
+ diffNotesToggle.trigger('click');
- spyOn(vm.$store, 'dispatch').and.stub();
-
- vm.$el.querySelector('.js-diff-notes-toggle').click();
-
- expect(vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
- discussionId: vm.discussions[0].id,
+ expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
+ discussionId: discussionsMockData.id,
});
});
- it('renders expand button when discussion is collapsed', done => {
- createComponent({ shouldCollapseDiscussions: true });
-
- vm.discussions[0].expanded = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.js-diff-notes-toggle').textContent.trim()).toBe('1');
- expect(vm.$el.querySelector('.js-diff-notes-toggle').className).toContain(
- 'btn-transparent badge badge-pill',
- );
+ it('renders expand button when discussion is collapsed', () => {
+ const discussions = getDiscussionsMockData();
+ discussions[0].expanded = false;
+ createComponent({ discussions, shouldCollapseDiscussions: true });
+ const diffNotesToggle = findDiffNotesToggle();
- done();
- });
+ expect(diffNotesToggle.text().trim()).toBe('1');
+ expect(diffNotesToggle.classes()).toEqual(
+ jasmine.arrayContaining(['btn-transparent', 'badge', 'badge-pill']),
+ );
});
- it('hides discussion when collapsed', done => {
- createComponent({ shouldCollapseDiscussions: true });
+ it('hides discussion when collapsed', () => {
+ const discussions = getDiscussionsMockData();
+ discussions[0].expanded = false;
+ createComponent({ discussions, shouldCollapseDiscussions: true });
- vm.discussions[0].expanded = false;
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.note-discussion').style.display).toBe('none');
-
- done();
- });
+ expect(wrapper.find(NoteableDiscussion).isVisible()).toBe(false);
});
it('renders badge on avatar', () => {
- createComponent({ renderAvatarBadge: true, discussions: [{ ...discussionsMockData }] });
-
- expect(vm.$el.querySelector('.user-avatar-link .badge-pill')).not.toBe(null);
- expect(vm.$el.querySelector('.user-avatar-link .badge-pill').textContent.trim()).toBe('1');
+ createComponent({ renderAvatarBadge: true });
+ const noteableDiscussion = wrapper.find(NoteableDiscussion);
+
+ expect(noteableDiscussion.find('.badge-pill').exists()).toBe(true);
+ expect(
+ noteableDiscussion
+ .find('.badge-pill')
+ .text()
+ .trim(),
+ ).toBe('1');
});
});
});
diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js
index 005a4751ea1..596a1ba5ad2 100644
--- a/spec/javascripts/diffs/components/diff_file_header_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_header_spec.js
@@ -3,7 +3,7 @@ import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
-import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import mountComponent, { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffDiscussionsMockData from '../mock_data/diff_discussions';
import { diffViewerModes } from '~/ide/constants';
@@ -249,6 +249,75 @@ describe('diff_file_header', () => {
expect(vm.$emit).not.toHaveBeenCalled();
});
});
+
+ describe('handleFileNameClick', () => {
+ let e;
+
+ beforeEach(() => {
+ e = { preventDefault: () => {} };
+ spyOn(e, 'preventDefault');
+ });
+
+ describe('when file name links to other page', () => {
+ it('does not call preventDefault if submodule tree url exists', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ diffFile: { ...props.diffFile, submodule_tree_url: 'foobar.com' },
+ });
+
+ vm.handleFileNameClick(e);
+
+ expect(e.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('does not call preventDefault if submodule_link exists', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ diffFile: { ...props.diffFile, submodule_link: 'foobar.com' },
+ });
+ vm.handleFileNameClick(e);
+
+ expect(e.preventDefault).not.toHaveBeenCalled();
+ });
+
+ it('does not call preventDefault if discussionPath exists', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ discussionPath: 'Foo bar',
+ });
+
+ vm.handleFileNameClick(e);
+
+ expect(e.preventDefault).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('scrolling to diff', () => {
+ let scrollToElement;
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ spyOn(document, 'querySelector').and.returnValue(el);
+ scrollToElement = spyOnDependency(DiffFileHeader, 'scrollToElement');
+ vm = mountComponent(Component, props);
+
+ vm.handleFileNameClick(e);
+ });
+
+ it('calls scrollToElement with file content', () => {
+ expect(scrollToElement).toHaveBeenCalledWith(el);
+ });
+
+ it('element adds the content id to the window location', () => {
+ expect(window.location.hash).toContain(props.diffFile.file_hash);
+ });
+
+ it('calls preventDefault when button does not link to other page', () => {
+ expect(e.preventDefault).toHaveBeenCalled();
+ });
+ });
+ });
});
describe('template', () => {
@@ -382,7 +451,7 @@ describe('diff_file_header', () => {
props.diffFile.edit_path = '/';
vm = mountComponentWithStore(Component, { props, store });
- expect(vm.$el.querySelector('.js-edit-blob')).toContainText('Edit');
+ expect(vm.$el.querySelector('.js-edit-blob')).not.toBe(null);
});
it('should not show edit button when file is deleted', () => {
@@ -491,5 +560,154 @@ describe('diff_file_header', () => {
});
});
});
+
+ describe('file actions', () => {
+ it('should not render if diff file has a submodule', () => {
+ props.diffFile.submodule = 'submodule';
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.file-actions')).toEqual(null);
+ });
+
+ it('should not render if add merge request buttons is false', () => {
+ props.addMergeRequestButtons = false;
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.file-actions')).toEqual(null);
+ });
+
+ describe('with add merge request buttons enabled', () => {
+ beforeEach(() => {
+ props.addMergeRequestButtons = true;
+ props.diffFile.edit_path = 'edit-path';
+ });
+
+ const viewReplacedFileButton = () => vm.$el.querySelector('.js-view-replaced-file');
+ const viewFileButton = () => vm.$el.querySelector('.js-view-file-button');
+ const externalUrl = () => vm.$el.querySelector('.js-external-url');
+
+ it('should render if add merge request buttons is true and diff file does not have a submodule', () => {
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.file-actions')).not.toEqual(null);
+ });
+
+ it('should not render view replaced file button if no replaced view path is present', () => {
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(viewReplacedFileButton()).toEqual(null);
+ });
+
+ it('should render view replaced file button if replaced view path is present', () => {
+ props.diffFile.replaced_view_path = 'replaced-view-path';
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(viewReplacedFileButton()).not.toEqual(null);
+ expect(viewReplacedFileButton().getAttribute('href')).toBe('replaced-view-path');
+ });
+
+ it('should render correct file view button path', () => {
+ props.diffFile.view_path = 'view-path';
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(viewFileButton().getAttribute('href')).toBe('view-path');
+ expect(viewFileButton().getAttribute('data-original-title')).toEqual(
+ `View file @ ${props.diffFile.content_sha.substr(0, 8)}`,
+ );
+ });
+
+ it('should not render external url view link if diff file has no external url', () => {
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(externalUrl()).toEqual(null);
+ });
+
+ it('should render external url view link if diff file has external url', () => {
+ props.diffFile.external_url = 'external_url';
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(externalUrl()).not.toEqual(null);
+ expect(externalUrl().getAttribute('href')).toBe('external_url');
+ });
+ });
+
+ describe('without file blob', () => {
+ beforeEach(() => {
+ props.diffFile.blob = null;
+ props.addMergeRequestButtons = true;
+ vm = mountComponentWithStore(Component, { props, store });
+ });
+
+ it('should not render toggle discussions button', () => {
+ expect(vm.$el.querySelector('.js-btn-vue-toggle-comments')).toEqual(null);
+ });
+
+ it('should not render edit button', () => {
+ expect(vm.$el.querySelector('.js-edit-blob')).toEqual(null);
+ });
+ });
+ });
+ });
+
+ describe('expand full file button', () => {
+ beforeEach(() => {
+ props.addMergeRequestButtons = true;
+ props.diffFile.edit_path = '/';
+ });
+
+ it('does not render button', () => {
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.js-expand-file')).toBe(null);
+ });
+
+ it('renders button', () => {
+ props.diffFile.is_fully_expanded = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.js-expand-file')).not.toBe(null);
+ });
+
+ it('shows fully expanded text', () => {
+ props.diffFile.is_fully_expanded = false;
+ props.diffFile.isShowingFullFile = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.ic-doc-changes')).not.toBeNull();
+ });
+
+ it('shows expand text', () => {
+ props.diffFile.is_fully_expanded = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.ic-doc-expand')).not.toBeNull();
+ });
+
+ it('renders loading icon', () => {
+ props.diffFile.is_fully_expanded = false;
+ props.diffFile.isLoadingFullFile = true;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ expect(vm.$el.querySelector('.js-expand-file .loading-container')).not.toBe(null);
+ });
+
+ it('calls toggleFullDiff on click', () => {
+ props.diffFile.is_fully_expanded = false;
+
+ vm = mountComponentWithStore(Component, { props, store });
+
+ spyOn(vm.$store, 'dispatch').and.stub();
+
+ vm.$el.querySelector('.js-expand-file').click();
+
+ expect(vm.$store.dispatch).toHaveBeenCalledWith(
+ 'diffs/toggleFullDiff',
+ props.diffFile.file_path,
+ );
+ });
});
});
diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index 65a1c9b8f15..ef4589ada48 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import DiffFileComponent from '~/diffs/components/diff_file.vue';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
-import store from '~/mr_notes/stores';
+import store from 'ee_else_ce/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -109,6 +109,31 @@ describe('DiffFile', () => {
done();
});
});
+
+ it('should update store state', done => {
+ spyOn(vm.$store, 'dispatch');
+
+ vm.isCollapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.$store.dispatch).toHaveBeenCalledWith('diffs/setFileCollapsed', {
+ filePath: vm.file.file_path,
+ collapsed: true,
+ });
+
+ done();
+ });
+ });
+
+ it('updates local state when changing file state', done => {
+ vm.file.viewer.collapsed = true;
+
+ vm.$nextTick(() => {
+ expect(vm.isCollapsed).toBe(true);
+
+ done();
+ });
+ });
});
});
@@ -116,18 +141,16 @@ describe('DiffFile', () => {
it('should have too large warning and blob link', done => {
const BLOB_LINK = '/file/view/path';
vm.file.viewer.error = diffViewerErrors.too_large;
+ vm.file.viewer.error_message =
+ 'This source diff could not be displayed because it is too large';
vm.file.view_path = BLOB_LINK;
+ vm.file.renderIt = true;
vm.$nextTick(() => {
expect(vm.$el.innerText).toContain(
'This source diff could not be displayed because it is too large',
);
- expect(vm.$el.querySelector('.js-too-large-diff')).toBeDefined();
- expect(
- vm.$el.querySelector('.js-too-large-diff a').href.indexOf(BLOB_LINK),
- ).toBeGreaterThan(-1);
-
done();
});
});
diff --git a/spec/javascripts/diffs/components/edit_button_spec.js b/spec/javascripts/diffs/components/edit_button_spec.js
deleted file mode 100644
index 7237274eb43..00000000000
--- a/spec/javascripts/diffs/components/edit_button_spec.js
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/hidden_files_warning_spec.js b/spec/javascripts/diffs/components/hidden_files_warning_spec.js
deleted file mode 100644
index 7237274eb43..00000000000
--- a/spec/javascripts/diffs/components/hidden_files_warning_spec.js
+++ /dev/null
@@ -1 +0,0 @@
-// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034
diff --git a/spec/javascripts/diffs/components/inline_diff_view_spec.js b/spec/javascripts/diffs/components/inline_diff_view_spec.js
index 2316ee29106..4452106580a 100644
--- a/spec/javascripts/diffs/components/inline_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/inline_diff_view_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import '~/behaviors/markdown/render_gfm';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
-import store from '~/mr_notes/stores';
+import store from 'ee_else_ce/mr_notes/stores';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions';
diff --git a/spec/javascripts/diffs/components/parallel_diff_view_spec.js b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
index 6f6b1c41915..236bda96145 100644
--- a/spec/javascripts/diffs/components/parallel_diff_view_spec.js
+++ b/spec/javascripts/diffs/components/parallel_diff_view_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
-import store from '~/mr_notes/stores';
+import store from 'ee_else_ce/mr_notes/stores';
import * as constants from '~/diffs/constants';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import diffFileMockData from '../mock_data/diff_file';
@@ -18,6 +18,10 @@ describe('ParallelDiffView', () => {
}).$mount();
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
describe('assigned', () => {
describe('diffLines', () => {
it('should normalize lines for empty cells', () => {
diff --git a/spec/javascripts/diffs/mock_data/diff_discussions.js b/spec/javascripts/diffs/mock_data/diff_discussions.js
index 4a091b4580b..711ab543411 100644
--- a/spec/javascripts/diffs/mock_data/diff_discussions.js
+++ b/spec/javascripts/diffs/mock_data/diff_discussions.js
@@ -288,6 +288,7 @@ export default {
external_storage: null,
old_path_html: 'CHANGELOG_OLD',
new_path_html: 'CHANGELOG',
+ is_fully_expanded: true,
context_lines_path:
'/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
highlighted_diff_lines: [
@@ -495,7 +496,7 @@ export default {
{
text: 'line',
rich_text:
- '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new noteable_line"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new noteable_line"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n',
+ '<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="1">\n1\n</td>\n<td class="line_content new"><span id="LC1" class="line" lang="plaintext"> - Bad dates</span>\n</td>\n</tr>\n<tr class="line_holder new" id="">\n<td class="diff-line-num new old_line" data-linenumber="1">\n \n</td>\n<td class="diff-line-num new new_line" data-linenumber="2">\n2\n</td>\n<td class="line_content new"><span id="LC2" class="line" lang="plaintext"></span>\n</td>\n</tr>\n',
can_receive_suggestion: true,
line_code: '6f209374f7e565f771b95720abf46024c41d1885_1_1',
type: 'new',
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
index 32af9ea8ddd..27428197c1c 100644
--- a/spec/javascripts/diffs/mock_data/diff_file.js
+++ b/spec/javascripts/diffs/mock_data/diff_file.js
@@ -240,4 +240,5 @@ export default {
},
],
discussions: [],
+ renderingLines: false,
};
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index acff80bca62..f129fbb57a3 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -30,6 +30,13 @@ import actions, {
setRenderTreeList,
setShowWhitespace,
setRenderIt,
+ requestFullDiff,
+ receiveFullDiffSucess,
+ receiveFullDiffError,
+ fetchFullDiff,
+ toggleFullDiff,
+ setFileCollapsed,
+ setExpandedDiffLines,
} from '~/diffs/store/actions';
import eventHub from '~/notes/event_hub';
import * as types from '~/diffs/store/mutation_types';
@@ -75,7 +82,7 @@ describe('DiffsStoreActions', () => {
describe('fetchDiffFiles', () => {
it('should fetch diff files', done => {
- const endpoint = '/fetch/diff/files';
+ const endpoint = '/fetch/diff/files?w=1';
const mock = new MockAdapter(axios);
const res = { diff_files: 1, merge_request_diffs: [] };
mock.onGet(endpoint).reply(200, res);
@@ -100,9 +107,10 @@ describe('DiffsStoreActions', () => {
});
describe('setHighlightedRow', () => {
- it('should set lineHash and fileHash of highlightedRow', () => {
+ 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.UPDATE_CURRENT_DIFF_FILE_ID, payload: 'ABC' },
]);
});
});
@@ -388,6 +396,7 @@ describe('DiffsStoreActions', () => {
});
describe('loadCollapsedDiff', () => {
+ const state = { showWhitespace: true };
it('should fetch data and call mutation with response and the give parameter', done => {
const file = { hash: 123, load_collapsed_diff_url: '/load/collapsed/diff/url' };
const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
@@ -395,7 +404,7 @@ describe('DiffsStoreActions', () => {
const commit = jasmine.createSpy('commit');
mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
- loadCollapsedDiff({ commit, getters: { commitId: null } }, file)
+ loadCollapsedDiff({ commit, getters: { commitId: null }, state }, file)
.then(() => {
expect(commit).toHaveBeenCalledWith(types.ADD_COLLAPSED_DIFFS, { file, data });
@@ -413,10 +422,10 @@ describe('DiffsStoreActions', () => {
spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
- loadCollapsedDiff({ commit() {}, getters }, file);
+ loadCollapsedDiff({ commit() {}, getters, state }, file);
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
- params: { commit_id: null },
+ params: { commit_id: null, w: '0' },
});
});
@@ -428,10 +437,10 @@ describe('DiffsStoreActions', () => {
spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
- loadCollapsedDiff({ commit() {}, getters }, file);
+ loadCollapsedDiff({ commit() {}, getters, state }, file);
expect(axios.get).toHaveBeenCalledWith(file.load_collapsed_diff_url, {
- params: { commit_id: '123' },
+ params: { commit_id: '123', w: '0' },
});
});
});
@@ -713,22 +722,6 @@ describe('DiffsStoreActions', () => {
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, 'test');
});
-
- it('resets currentDiffId after timeout', () => {
- const state = {
- treeEntries: {
- path: {
- fileHash: 'test',
- },
- },
- };
-
- scrollToFile({ state, commit }, 'path');
-
- jasmine.clock().tick(1000);
-
- expect(commit.calls.argsFor(1)).toEqual([types.UPDATE_CURRENT_DIFF_FILE_ID, '']);
- });
});
describe('toggleShowTreeList', () => {
@@ -743,6 +736,14 @@ describe('DiffsStoreActions', () => {
expect(localStorage.setItem).toHaveBeenCalledWith('mr_tree_show', true);
});
+
+ it('does not update localStorage', () => {
+ spyOn(localStorage, 'setItem');
+
+ toggleShowTreeList({ commit() {}, state: { showTreeList: true } }, false);
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
});
describe('renderFileForDiscussionId', () => {
@@ -828,6 +829,10 @@ describe('DiffsStoreActions', () => {
});
describe('setShowWhitespace', () => {
+ beforeEach(() => {
+ spyOn(eventHub, '$emit').and.stub();
+ });
+
it('commits SET_SHOW_WHITESPACE', done => {
testAction(
setShowWhitespace,
@@ -855,6 +860,30 @@ describe('DiffsStoreActions', () => {
expect(window.history.pushState).toHaveBeenCalled();
});
+
+ it('calls history pushState with merged params', () => {
+ const originalPushState = window.history;
+
+ originalPushState.pushState({}, '', '?test=1');
+
+ spyOn(localStorage, 'setItem').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
+
+ setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
+
+ expect(window.history.pushState.calls.mostRecent().args[2]).toMatch(/(.*)\?test=1&w=0/);
+
+ originalPushState.pushState({}, '', '?');
+ });
+
+ it('emits eventHub event', () => {
+ spyOn(localStorage, 'setItem').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
+
+ setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refetchDiffData');
+ });
});
describe('setRenderIt', () => {
@@ -862,4 +891,193 @@ describe('DiffsStoreActions', () => {
testAction(setRenderIt, 'file', {}, [{ type: types.RENDER_FILE, payload: 'file' }], [], done);
});
});
+
+ describe('requestFullDiff', () => {
+ it('commits REQUEST_FULL_DIFF', done => {
+ testAction(
+ requestFullDiff,
+ 'file',
+ {},
+ [{ type: types.REQUEST_FULL_DIFF, payload: 'file' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveFullDiffSucess', () => {
+ it('commits REQUEST_FULL_DIFF', done => {
+ testAction(
+ receiveFullDiffSucess,
+ { filePath: 'test' },
+ {},
+ [{ type: types.RECEIVE_FULL_DIFF_SUCCESS, payload: { filePath: 'test' } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveFullDiffError', () => {
+ it('commits REQUEST_FULL_DIFF', done => {
+ testAction(
+ receiveFullDiffError,
+ 'file',
+ {},
+ [{ type: types.RECEIVE_FULL_DIFF_ERROR, payload: 'file' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchFullDiff', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('success', () => {
+ beforeEach(() => {
+ mock.onGet(`${gl.TEST_HOST}/context`).replyOnce(200, ['test']);
+ });
+
+ it('dispatches receiveFullDiffSucess', done => {
+ const file = {
+ context_lines_path: `${gl.TEST_HOST}/context`,
+ file_path: 'test',
+ file_hash: 'test',
+ };
+ testAction(
+ fetchFullDiff,
+ file,
+ null,
+ [],
+ [
+ { type: 'receiveFullDiffSucess', payload: { filePath: 'test' } },
+ { type: 'setExpandedDiffLines', payload: { file, data: ['test'] } },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(`${gl.TEST_HOST}/context`).replyOnce(500);
+ });
+
+ it('dispatches receiveFullDiffError', done => {
+ testAction(
+ fetchFullDiff,
+ { context_lines_path: `${gl.TEST_HOST}/context`, file_path: 'test', file_hash: 'test' },
+ null,
+ [],
+ [{ type: 'receiveFullDiffError', payload: 'test' }],
+ done,
+ );
+ });
+ });
+ });
+
+ describe('toggleFullDiff', () => {
+ let state;
+
+ beforeEach(() => {
+ state = {
+ diffFiles: [{ file_path: 'test', isShowingFullFile: false }],
+ };
+ });
+
+ it('dispatches fetchFullDiff when file is not expanded', done => {
+ testAction(
+ toggleFullDiff,
+ 'test',
+ state,
+ [],
+ [
+ { type: 'requestFullDiff', payload: 'test' },
+ { type: 'fetchFullDiff', payload: state.diffFiles[0] },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('setFileCollapsed', () => {
+ it('commits SET_FILE_COLLAPSED', done => {
+ testAction(
+ setFileCollapsed,
+ { filePath: 'test', collapsed: true },
+ null,
+ [{ type: types.SET_FILE_COLLAPSED, payload: { filePath: 'test', collapsed: true } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setExpandedDiffLines', () => {
+ beforeEach(() => {
+ spyOnDependency(actions, 'idleCallback').and.callFake(cb => {
+ cb({ timeRemaining: () => 50 });
+ });
+ });
+
+ it('commits SET_CURRENT_VIEW_DIFF_FILE_LINES when lines less than MAX_RENDERING_DIFF_LINES', done => {
+ spyOnDependency(actions, 'convertExpandLines').and.callFake(() => ['test']);
+
+ testAction(
+ setExpandedDiffLines,
+ { file: { file_path: 'path' }, data: [] },
+ { diffViewType: 'inline' },
+ [
+ {
+ type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', lines: ['test'] },
+ },
+ {
+ type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', lines: ['test'] },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('commits ADD_CURRENT_VIEW_DIFF_FILE_LINES when lines more than MAX_RENDERING_DIFF_LINES', done => {
+ const lines = new Array(501).fill().map((_, i) => `line-${i}`);
+ spyOnDependency(actions, 'convertExpandLines').and.callFake(() => lines);
+
+ testAction(
+ setExpandedDiffLines,
+ { file: { file_path: 'path' }, data: [] },
+ { diffViewType: 'inline' },
+ [
+ {
+ type: 'SET_HIDDEN_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', lines },
+ },
+ {
+ type: 'SET_CURRENT_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', lines: lines.slice(0, 200) },
+ },
+ { type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' },
+ ...new Array(301).fill().map((_, i) => ({
+ type: 'ADD_CURRENT_VIEW_DIFF_FILE_LINES',
+ payload: { filePath: 'path', line: `line-${i + 200}` },
+ })),
+ { type: 'TOGGLE_DIFF_FILE_RENDERING_MORE', payload: 'path' },
+ ],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/getters_spec.js b/spec/javascripts/diffs/store/getters_spec.js
index 0ab88e6b2aa..eab5703dfb2 100644
--- a/spec/javascripts/diffs/store/getters_spec.js
+++ b/spec/javascripts/diffs/store/getters_spec.js
@@ -270,4 +270,24 @@ describe('Diffs Module Getters', () => {
expect(getters.diffFilesLength(localState)).toBe(2);
});
});
+
+ describe('currentDiffIndex', () => {
+ it('returns index of currently selected diff in diffList', () => {
+ localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
+ localState.currentDiffFileId = '222';
+
+ expect(getters.currentDiffIndex(localState)).toEqual(1);
+
+ localState.currentDiffFileId = '333';
+
+ expect(getters.currentDiffIndex(localState)).toEqual(2);
+ });
+
+ it('returns 0 if no diff is selected yet or diff is not found', () => {
+ localState.diffFiles = [{ file_hash: '111' }, { file_hash: '222' }, { file_hash: '333' }];
+ localState.currentDiffFileId = '';
+
+ expect(getters.currentDiffIndex(localState)).toEqual(0);
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 09ee691b602..fa193e1d3b9 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -58,13 +58,15 @@ describe('DiffsStoreMutations', () => {
describe('EXPAND_ALL_FILES', () => {
it('should change the collapsed prop from diffFiles', () => {
const diffFile = {
- collapsed: true,
+ viewer: {
+ collapsed: true,
+ },
};
const state = { expandAllFiles: true, diffFiles: [diffFile] };
mutations[types.EXPAND_ALL_FILES](state);
- expect(state.diffFiles[0].collapsed).toEqual(false);
+ expect(state.diffFiles[0].viewer.collapsed).toEqual(false);
});
});
@@ -680,4 +682,172 @@ describe('DiffsStoreMutations', () => {
expect(state.showWhitespace).toBe(false);
});
});
+
+ describe('REQUEST_FULL_DIFF', () => {
+ it('sets isLoadingFullFile to true', () => {
+ const state = {
+ diffFiles: [{ file_path: 'test', isLoadingFullFile: false }],
+ };
+
+ mutations[types.REQUEST_FULL_DIFF](state, 'test');
+
+ expect(state.diffFiles[0].isLoadingFullFile).toBe(true);
+ });
+ });
+
+ describe('RECEIVE_FULL_DIFF_ERROR', () => {
+ it('sets isLoadingFullFile to false', () => {
+ const state = {
+ diffFiles: [{ file_path: 'test', isLoadingFullFile: true }],
+ };
+
+ mutations[types.RECEIVE_FULL_DIFF_ERROR](state, 'test');
+
+ expect(state.diffFiles[0].isLoadingFullFile).toBe(false);
+ });
+ });
+
+ describe('RECEIVE_FULL_DIFF_SUCCESS', () => {
+ it('sets isLoadingFullFile to false', () => {
+ const state = {
+ diffFiles: [
+ {
+ file_path: 'test',
+ isLoadingFullFile: true,
+ isShowingFullFile: false,
+ highlighted_diff_lines: [],
+ parallel_diff_lines: [],
+ },
+ ],
+ };
+
+ mutations[types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath: 'test', data: [] });
+
+ expect(state.diffFiles[0].isLoadingFullFile).toBe(false);
+ });
+
+ it('sets isShowingFullFile to true', () => {
+ const state = {
+ diffFiles: [
+ {
+ file_path: 'test',
+ isLoadingFullFile: true,
+ isShowingFullFile: false,
+ highlighted_diff_lines: [],
+ parallel_diff_lines: [],
+ },
+ ],
+ };
+
+ mutations[types.RECEIVE_FULL_DIFF_SUCCESS](state, { filePath: 'test', data: [] });
+
+ expect(state.diffFiles[0].isShowingFullFile).toBe(true);
+ });
+ });
+
+ describe('SET_FILE_COLLAPSED', () => {
+ it('sets collapsed', () => {
+ const state = {
+ diffFiles: [{ file_path: 'test', viewer: { collapsed: false } }],
+ };
+
+ mutations[types.SET_FILE_COLLAPSED](state, { filePath: 'test', collapsed: true });
+
+ expect(state.diffFiles[0].viewer.collapsed).toBe(true);
+ });
+ });
+
+ describe('SET_HIDDEN_VIEW_DIFF_FILE_LINES', () => {
+ [
+ { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
+ { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
+ ].forEach(({ current, hidden, diffViewType }) => {
+ it(`sets the ${hidden} lines when diff view is ${diffViewType}`, () => {
+ const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
+ const state = {
+ diffFiles: [file],
+ diffViewType,
+ };
+
+ mutations[types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ lines: ['test'],
+ });
+
+ expect(file[`${current}_diff_lines`]).toEqual([]);
+ expect(file[`${hidden}_diff_lines`]).toEqual(['test']);
+ });
+ });
+ });
+
+ describe('SET_CURRENT_VIEW_DIFF_FILE_LINES', () => {
+ [
+ { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
+ { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
+ ].forEach(({ current, hidden, diffViewType }) => {
+ it(`sets the ${current} lines when diff view is ${diffViewType}`, () => {
+ const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
+ const state = {
+ diffFiles: [file],
+ diffViewType,
+ };
+
+ mutations[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ lines: ['test'],
+ });
+
+ expect(file[`${current}_diff_lines`]).toEqual(['test']);
+ expect(file[`${hidden}_diff_lines`]).toEqual([]);
+ });
+ });
+ });
+
+ describe('ADD_CURRENT_VIEW_DIFF_FILE_LINES', () => {
+ [
+ { current: 'highlighted', hidden: 'parallel', diffViewType: 'inline' },
+ { current: 'parallel', hidden: 'highlighted', diffViewType: 'parallel' },
+ ].forEach(({ current, hidden, diffViewType }) => {
+ it(`pushes to ${current} lines when diff view is ${diffViewType}`, () => {
+ const file = { file_path: 'test', parallel_diff_lines: [], highlighted_diff_lines: [] };
+ const state = {
+ diffFiles: [file],
+ diffViewType,
+ };
+
+ mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ line: 'test',
+ });
+
+ expect(file[`${current}_diff_lines`]).toEqual(['test']);
+ expect(file[`${hidden}_diff_lines`]).toEqual([]);
+
+ mutations[types.ADD_CURRENT_VIEW_DIFF_FILE_LINES](state, {
+ filePath: 'test',
+ line: 'test2',
+ });
+
+ expect(file[`${current}_diff_lines`]).toEqual(['test', 'test2']);
+ expect(file[`${hidden}_diff_lines`]).toEqual([]);
+ });
+ });
+ });
+
+ describe('TOGGLE_DIFF_FILE_RENDERING_MORE', () => {
+ it('toggles renderingLines on file', () => {
+ const file = { file_path: 'test', renderingLines: false };
+ const state = {
+ diffFiles: [file],
+ };
+
+ mutations[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, 'test');
+
+ expect(file.renderingLines).toBe(true);
+
+ mutations[types.TOGGLE_DIFF_FILE_RENDERING_MORE](state, 'test');
+
+ expect(file.renderingLines).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js
index 599ea9cd420..1f877910125 100644
--- a/spec/javascripts/diffs/store/utils_spec.js
+++ b/spec/javascripts/diffs/store/utils_spec.js
@@ -781,4 +781,49 @@ describe('DiffsStoreUtils', () => {
]);
});
});
+
+ describe('convertExpandLines', () => {
+ it('converts expanded lines to normal lines', () => {
+ const diffLines = [
+ {
+ type: 'match',
+ old_line: 1,
+ new_line: 1,
+ },
+ {
+ type: '',
+ old_line: 2,
+ new_line: 2,
+ },
+ ];
+
+ const lines = utils.convertExpandLines({
+ diffLines,
+ data: [{ text: 'expanded' }],
+ typeKey: 'type',
+ oldLineKey: 'old_line',
+ newLineKey: 'new_line',
+ mapLine: ({ line, oldLine, newLine }) => ({
+ ...line,
+ old_line: oldLine,
+ new_line: newLine,
+ }),
+ });
+
+ expect(lines).toEqual([
+ {
+ text: 'expanded',
+ new_line: 1,
+ old_line: 1,
+ discussions: [],
+ hasForm: false,
+ },
+ {
+ type: '',
+ old_line: 2,
+ new_line: 2,
+ },
+ ]);
+ });
+ });
});
diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
index ae2a785de52..b1017e0c4f0 100644
--- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
+++ b/spec/javascripts/dirty_submit/dirty_submit_form_spec.js
@@ -1,3 +1,4 @@
+import _ from 'underscore';
import DirtySubmitForm from '~/dirty_submit/dirty_submit_form';
import { getInputValue, setInputValue, createForm } from './helper';
@@ -13,44 +14,100 @@ function expectToToggleDisableOnDirtyUpdate(submit, input) {
}
describe('DirtySubmitForm', () => {
- it('disables submit until there are changes', done => {
- const { form, input, submit } = createForm();
+ const originalThrottleDuration = DirtySubmitForm.THROTTLE_DURATION;
- new DirtySubmitForm(form); // eslint-disable-line no-new
+ describe('submit button tests', () => {
+ beforeEach(() => {
+ DirtySubmitForm.THROTTLE_DURATION = 0;
+ });
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
- });
+ afterEach(() => {
+ DirtySubmitForm.THROTTLE_DURATION = originalThrottleDuration;
+ });
- it('disables submit until there are changes when initializing with a falsy value', done => {
- const { form, input, submit } = createForm();
- input.value = '';
+ it('disables submit until there are changes', done => {
+ const { form, input, submit } = createForm();
- new DirtySubmitForm(form); // eslint-disable-line no-new
+ new DirtySubmitForm(form); // eslint-disable-line no-new
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
- });
+ return expectToToggleDisableOnDirtyUpdate(submit, input)
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('disables submit until there are changes when initializing with a falsy value', done => {
+ const { form, input, submit } = createForm();
+ input.value = '';
+
+ new DirtySubmitForm(form); // eslint-disable-line no-new
+
+ return expectToToggleDisableOnDirtyUpdate(submit, input)
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('disables submit until there are changes for radio inputs', done => {
+ const { form, input, submit } = createForm('radio');
+
+ new DirtySubmitForm(form); // eslint-disable-line no-new
+
+ return expectToToggleDisableOnDirtyUpdate(submit, input)
+ .then(done)
+ .catch(done.fail);
+ });
- it('disables submit until there are changes for radio inputs', done => {
- const { form, input, submit } = createForm('radio');
+ it('disables submit until there are changes for checkbox inputs', done => {
+ const { form, input, submit } = createForm('checkbox');
- new DirtySubmitForm(form); // eslint-disable-line no-new
+ new DirtySubmitForm(form); // eslint-disable-line no-new
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
+ return expectToToggleDisableOnDirtyUpdate(submit, input)
+ .then(done)
+ .catch(done.fail);
+ });
});
- it('disables submit until there are changes for checkbox inputs', done => {
- const { form, input, submit } = createForm('checkbox');
+ describe('throttling tests', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ DirtySubmitForm.THROTTLE_DURATION = 100;
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ DirtySubmitForm.THROTTLE_DURATION = originalThrottleDuration;
+ });
+
+ it('throttles updates when rapid changes are made to a single form element', () => {
+ const { form, input } = createForm();
+ const updateDirtyInputSpy = spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
+
+ _.range(10).forEach(i => {
+ setInputValue(input, `change ${i}`, false);
+ });
+
+ jasmine.clock().tick(101);
+
+ expect(updateDirtyInputSpy).toHaveBeenCalledTimes(2);
+ });
+
+ it('does not throttle updates when rapid changes are made to different form elements', () => {
+ const form = document.createElement('form');
+ const range = _.range(10);
+ range.forEach(i => {
+ form.innerHTML += `<input type="text" name="input-${i}" class="js-input-${i}"/>`;
+ });
+
+ const updateDirtyInputSpy = spyOn(new DirtySubmitForm(form), 'updateDirtyInput');
+
+ range.forEach(i => {
+ const input = form.querySelector(`.js-input-${i}`);
+ setInputValue(input, `change`, false);
+ });
- new DirtySubmitForm(form); // eslint-disable-line no-new
+ jasmine.clock().tick(101);
- return expectToToggleDisableOnDirtyUpdate(submit, input)
- .then(done)
- .catch(done.fail);
+ expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length);
+ });
});
});
diff --git a/spec/javascripts/environments/confirm_rollback_modal_spec.js b/spec/javascripts/environments/confirm_rollback_modal_spec.js
new file mode 100644
index 00000000000..05715bce38f
--- /dev/null
+++ b/spec/javascripts/environments/confirm_rollback_modal_spec.js
@@ -0,0 +1,70 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
+import eventHub from '~/environments/event_hub';
+
+describe('Confirm Rollback Modal Component', () => {
+ let environment;
+
+ beforeEach(() => {
+ environment = {
+ name: 'test',
+ last_deployment: {
+ commit: {
+ short_id: 'abc0123',
+ },
+ },
+ modalId: 'test',
+ };
+ });
+
+ it('should show "Rollback" when isLastDeployment is false', () => {
+ const component = shallowMount(ConfirmRollbackModal, {
+ propsData: {
+ environment: {
+ ...environment,
+ isLastDeployment: false,
+ },
+ },
+ });
+ const modal = component.find(GlModal);
+
+ expect(modal.attributes('title')).toContain('Rollback');
+ expect(modal.attributes('title')).toContain('test');
+ expect(modal.attributes('ok-title')).toBe('Rollback');
+ expect(modal.text()).toContain('commit abc0123');
+ expect(modal.text()).toContain('Are you sure you want to continue?');
+ });
+
+ it('should show "Re-deploy" when isLastDeployment is true', () => {
+ const component = shallowMount(ConfirmRollbackModal, {
+ propsData: {
+ environment: {
+ ...environment,
+ isLastDeployment: true,
+ },
+ },
+ });
+ const modal = component.find(GlModal);
+
+ expect(modal.attributes('title')).toContain('Re-deploy');
+ expect(modal.attributes('title')).toContain('test');
+ expect(modal.attributes('ok-title')).toBe('Re-deploy');
+ expect(modal.text()).toContain('commit abc0123');
+ expect(modal.text()).toContain('Are you sure you want to continue?');
+ });
+
+ it('should emit the "rollback" event when "ok" is clicked', () => {
+ environment = { ...environment, isLastDeployment: true };
+ const component = shallowMount(ConfirmRollbackModal, {
+ propsData: {
+ environment,
+ },
+ });
+ const eventHubSpy = spyOn(eventHub, '$emit');
+ const modal = component.find(GlModal);
+ modal.vm.$emit('ok');
+
+ expect(eventHubSpy).toHaveBeenCalledWith('rollbackEnvironment', environment);
+ });
+});
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index a89e50045da..388d7063d13 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -20,23 +20,23 @@ describe('Environment item', () => {
size: 3,
isFolder: true,
environment_path: 'url',
+ log_path: 'url',
};
component = new EnvironmentItem({
propsData: {
model: mockItem,
canReadEnvironment: true,
- service: {},
},
}).$mount();
});
- it('Should render folder icon and name', () => {
+ it('should render folder icon and name', () => {
expect(component.$el.querySelector('.folder-name').textContent).toContain(mockItem.name);
expect(component.$el.querySelector('.folder-icon')).toBeDefined();
});
- it('Should render the number of children in a badge', () => {
+ it('should render the number of children in a badge', () => {
expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(
mockItem.size,
);
@@ -60,7 +60,7 @@ describe('Environment item', () => {
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
- ref_path: 'root/ci-folders/tree/master',
+ ref_url: 'root/ci-folders/tree/master',
},
tag: true,
'last?': true,
@@ -109,6 +109,7 @@ describe('Environment item', () => {
},
has_stop_action: true,
environment_path: 'root/ci-folders/environments/31',
+ log_path: 'root/ci-folders/environments/31/logs',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
};
@@ -117,7 +118,6 @@ describe('Environment item', () => {
propsData: {
model: environment,
canReadEnvironment: true,
- service: {},
},
}).$mount();
});
@@ -157,13 +157,13 @@ describe('Environment item', () => {
});
describe('With build url', () => {
- it('Should link to build url provided', () => {
+ it('should link to build url provided', () => {
expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
environment.last_deployment.deployable.build_path,
);
});
- it('Should render deployable name and id', () => {
+ it('should render deployable name and id', () => {
expect(component.$el.querySelector('.build-link').getAttribute('href')).toEqual(
environment.last_deployment.deployable.build_path,
);
@@ -178,7 +178,7 @@ describe('Environment item', () => {
});
describe('With manual actions', () => {
- it('Should render actions component', () => {
+ it('should render actions component', () => {
expect(component.$el.querySelector('.js-manual-actions-container')).toBeDefined();
});
});
@@ -190,13 +190,13 @@ describe('Environment item', () => {
});
describe('With stop action', () => {
- it('Should render stop action component', () => {
+ it('should render stop action component', () => {
expect(component.$el.querySelector('.js-stop-component-container')).toBeDefined();
});
});
describe('With retry action', () => {
- it('Should render rollback component', () => {
+ it('should render rollback component', () => {
expect(component.$el.querySelector('.js-rollback-component-container')).toBeDefined();
});
});
diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/javascripts/environments/environment_rollback_spec.js
index 79f33c5bc8a..8c47f6a12c0 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js
+++ b/spec/javascripts/environments/environment_rollback_spec.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import eventHub from '~/environments/event_hub';
import rollbackComp from '~/environments/components/environment_rollback.vue';
describe('Rollback Component', () => {
- const retryURL = 'https://gitlab.com/retry';
+ const retryUrl = 'https://gitlab.com/retry';
let RollbackComponent;
beforeEach(() => {
@@ -13,8 +16,9 @@ describe('Rollback Component', () => {
const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
- retryUrl: retryURL,
+ retryUrl,
isLastDeployment: true,
+ environment: {},
},
}).$mount();
@@ -25,11 +29,33 @@ describe('Rollback Component', () => {
const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
- retryUrl: retryURL,
+ retryUrl,
isLastDeployment: false,
+ environment: {},
},
}).$mount();
expect(component.$el).toHaveSpriteIcon('redo');
});
+
+ it('should emit a "rollback" event on button click', () => {
+ const eventHubSpy = spyOn(eventHub, '$emit');
+ const component = shallowMount(RollbackComponent, {
+ propsData: {
+ retryUrl,
+ environment: {
+ name: 'test',
+ },
+ },
+ });
+ const button = component.find(GlButton);
+
+ button.vm.$emit('click');
+
+ expect(eventHubSpy).toHaveBeenCalledWith('requestRollbackEnvironment', {
+ retryUrl,
+ isLastDeployment: true,
+ name: 'test',
+ });
+ });
});
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index 52895f35f3a..a3f34232a85 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -6,6 +6,14 @@ describe('Environment table', () => {
let Component;
let vm;
+ const eeOnlyProps = {
+ canaryDeploymentFeatureId: 'canary_deployment',
+ showCanaryDeploymentCallout: true,
+ userCalloutsPath: '/callouts',
+ lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
+ helpCanaryDeploymentsPath: 'help/canary-deployments',
+ };
+
beforeEach(() => {
Component = Vue.extend(environmentTableComp);
});
@@ -27,8 +35,234 @@ describe('Environment table', () => {
vm = mountComponent(Component, {
environments: [mockItem],
canReadEnvironment: true,
+ ...eeOnlyProps,
});
expect(vm.$el.getAttribute('class')).toContain('ci-table');
});
+
+ describe('sortEnvironments', () => {
+ it('should sort environments by last updated', () => {
+ const mockItems = [
+ {
+ name: 'old',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2019, 0, 5).toISOString(),
+ },
+ },
+ {
+ name: 'new',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2019, 1, 5).toISOString(),
+ },
+ },
+ {
+ name: 'older',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2018, 0, 5).toISOString(),
+ },
+ },
+ {
+ name: 'an environment with no deployment',
+ },
+ ];
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ ...eeOnlyProps,
+ });
+
+ const [old, newer, older, noDeploy] = mockItems;
+
+ expect(vm.sortEnvironments(mockItems)).toEqual([newer, old, older, noDeploy]);
+ });
+
+ it('should push environments with no deployments to the bottom', () => {
+ const mockItems = [
+ {
+ name: 'production',
+ size: 1,
+ id: 2,
+ state: 'available',
+ external_url: 'https://google.com/production',
+ environment_type: null,
+ last_deployment: null,
+ has_stop_action: false,
+ environment_path: '/Commit451/lab-coat/environments/2',
+ stop_path: '/Commit451/lab-coat/environments/2/stop',
+ folder_path: '/Commit451/lab-coat/environments/folders/production',
+ created_at: '2019-01-17T16:26:10.064Z',
+ updated_at: '2019-01-17T16:27:37.717Z',
+ can_stop: true,
+ },
+ {
+ name: 'review/225addcibuildstatus',
+ size: 2,
+ isFolder: true,
+ isLoadingFolderContent: false,
+ folderName: 'review',
+ isOpen: false,
+ children: [],
+ id: 12,
+ state: 'available',
+ external_url: 'https://google.com/review/225addcibuildstatus',
+ environment_type: 'review',
+ last_deployment: null,
+ has_stop_action: false,
+ environment_path: '/Commit451/lab-coat/environments/12',
+ stop_path: '/Commit451/lab-coat/environments/12/stop',
+ folder_path: '/Commit451/lab-coat/environments/folders/review',
+ created_at: '2019-01-17T16:27:37.877Z',
+ updated_at: '2019-01-17T16:27:37.883Z',
+ can_stop: true,
+ },
+ {
+ name: 'staging',
+ size: 1,
+ id: 1,
+ state: 'available',
+ external_url: 'https://google.com/staging',
+ environment_type: null,
+ last_deployment: {
+ created_at: '2019-01-17T16:26:15.125Z',
+ scheduled_actions: [],
+ },
+ },
+ ];
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ ...eeOnlyProps,
+ });
+
+ const [prod, review, staging] = mockItems;
+
+ expect(vm.sortEnvironments(mockItems)).toEqual([review, staging, prod]);
+ });
+
+ it('should sort environments by folder first', () => {
+ const mockItems = [
+ {
+ name: 'old',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2019, 0, 5).toISOString(),
+ },
+ },
+ {
+ name: 'new',
+ size: 3,
+ isFolder: false,
+ last_deployment: {
+ created_at: new Date(2019, 1, 5).toISOString(),
+ },
+ },
+ {
+ name: 'older',
+ size: 3,
+ isFolder: true,
+ children: [],
+ },
+ ];
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ ...eeOnlyProps,
+ });
+
+ const [old, newer, older] = mockItems;
+
+ expect(vm.sortEnvironments(mockItems)).toEqual([older, newer, old]);
+ });
+
+ it('should break ties by name', () => {
+ const mockItems = [
+ {
+ name: 'old',
+ isFolder: false,
+ },
+ {
+ name: 'new',
+ isFolder: false,
+ },
+ {
+ folderName: 'older',
+ isFolder: true,
+ },
+ ];
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ ...eeOnlyProps,
+ });
+
+ const [old, newer, older] = mockItems;
+
+ expect(vm.sortEnvironments(mockItems)).toEqual([older, newer, old]);
+ });
+ });
+
+ describe('sortedEnvironments', () => {
+ it('it should sort children as well', () => {
+ const mockItems = [
+ {
+ name: 'production',
+ last_deployment: null,
+ },
+ {
+ name: 'review/225addcibuildstatus',
+ isFolder: true,
+ folderName: 'review',
+ isOpen: true,
+ children: [
+ {
+ name: 'review/225addcibuildstatus',
+ last_deployment: {
+ created_at: '2019-01-17T16:26:15.125Z',
+ },
+ },
+ {
+ name: 'review/master',
+ last_deployment: {
+ created_at: '2019-02-17T16:26:15.125Z',
+ },
+ },
+ ],
+ },
+ {
+ name: 'staging',
+ last_deployment: {
+ created_at: '2019-01-17T16:26:15.125Z',
+ },
+ },
+ ];
+ const [production, review, staging] = mockItems;
+ const [addcibuildstatus, master] = mockItems[1].children;
+
+ vm = mountComponent(Component, {
+ environments: mockItems,
+ canReadEnvironment: true,
+ ...eeOnlyProps,
+ });
+
+ expect(vm.sortedEnvironments.map(env => env.name)).toEqual([
+ review.name,
+ staging.name,
+ production.name,
+ ]);
+
+ expect(vm.sortedEnvironments[0].children).toEqual([master, addcibuildstatus]);
+ });
+ });
});
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index 9220f7a264f..0dcd8868aba 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -13,6 +13,11 @@ describe('Environment', () => {
cssContainerClass: 'container',
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
+ canaryDeploymentFeatureId: 'canary_deployment',
+ showCanaryDeploymentCallout: true,
+ userCalloutsPath: '/callouts',
+ lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
+ helpCanaryDeploymentsPath: 'help/canary-deployments',
};
let EnvironmentsComponent;
@@ -94,7 +99,7 @@ describe('Environment', () => {
it('should make an API request when page is clicked', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
- component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
+ component.$el.querySelector('.gl-pagination li:nth-child(5) .page-link').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
done();
diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js
index c3d16f10d72..8abdbcbbe54 100644
--- a/spec/javascripts/environments/environments_store_spec.js
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -34,54 +34,46 @@ describe('Store', () => {
expect(store.state.stoppedCounter).toEqual(2);
});
- describe('store environments', () => {
- it('should store environments', () => {
- store.storeEnvironments(serverData);
-
- expect(store.state.environments.length).toEqual(serverData.length);
- });
-
- it('should add folder keys when environment is a folder', () => {
- const environment = {
- name: 'bar',
- size: 3,
- id: 2,
- };
+ it('should add folder keys when environment is a folder', () => {
+ const environment = {
+ name: 'bar',
+ size: 3,
+ id: 2,
+ };
- store.storeEnvironments([environment]);
+ store.storeEnvironments([environment]);
- expect(store.state.environments[0].isFolder).toEqual(true);
- expect(store.state.environments[0].folderName).toEqual('bar');
- });
-
- it('should extract content of `latest` key when provided', () => {
- const environment = {
- name: 'bar',
- size: 3,
- id: 2,
- latest: {
- last_deployment: {},
- isStoppable: true,
- },
- };
-
- store.storeEnvironments([environment]);
+ expect(store.state.environments[0].isFolder).toEqual(true);
+ expect(store.state.environments[0].folderName).toEqual('bar');
+ });
- expect(store.state.environments[0].last_deployment).toEqual({});
- expect(store.state.environments[0].isStoppable).toEqual(true);
- });
+ it('should extract content of `latest` key when provided', () => {
+ const environment = {
+ name: 'bar',
+ size: 3,
+ id: 2,
+ latest: {
+ last_deployment: {},
+ isStoppable: true,
+ },
+ };
+
+ store.storeEnvironments([environment]);
+
+ expect(store.state.environments[0].last_deployment).toEqual({});
+ expect(store.state.environments[0].isStoppable).toEqual(true);
+ });
- it('should store latest.name when the environment is not a folder', () => {
- store.storeEnvironments(serverData);
+ it('should store latest.name when the environment is not a folder', () => {
+ store.storeEnvironments(serverData);
- expect(store.state.environments[0].name).toEqual(serverData[0].latest.name);
- });
+ expect(store.state.environments[0].name).toEqual(serverData[0].latest.name);
+ });
- it('should store root level name when environment is a folder', () => {
- store.storeEnvironments(serverData);
+ it('should store root level name when environment is a folder', () => {
+ store.storeEnvironments(serverData);
- expect(store.state.environments[1].folderName).toEqual(serverData[1].name);
- });
+ expect(store.state.environments[1].folderName).toEqual(serverData[1].name);
});
describe('toggleFolder', () => {
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index d9ee7e74e28..f1c323df4be 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { removeBreakLine, removeWhitespace } from 'spec/helpers/text_helper';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
@@ -15,6 +16,11 @@ describe('Environments Folder View', () => {
folderName: 'review',
canReadEnvironment: true,
cssContainerClass: 'container',
+ canaryDeploymentFeatureId: 'canary_deployment',
+ showCanaryDeploymentCallout: true,
+ userCalloutsPath: '/callouts',
+ lockPromotionSvgPath: '/assets/illustrations/lock-promotion.svg',
+ helpCanaryDeploymentsPath: 'help/canary-deployments',
};
beforeEach(() => {
@@ -89,9 +95,11 @@ describe('Environments Folder View', () => {
it('should render parent folder name', done => {
setTimeout(() => {
- expect(component.$el.querySelector('.js-folder-name').textContent.trim()).toContain(
- 'Environments / review',
- );
+ expect(
+ removeBreakLine(
+ removeWhitespace(component.$el.querySelector('.js-folder-name').textContent.trim()),
+ ),
+ ).toContain('Environments / review');
done();
}, 0);
});
@@ -107,7 +115,7 @@ describe('Environments Folder View', () => {
it('should make an API request when changing page', done => {
spyOn(component, 'updateContent');
setTimeout(() => {
- component.$el.querySelector('.gl-pagination .js-last-button a').click();
+ component.$el.querySelector('.gl-pagination .js-last-button .page-link').click();
expect(component.updateContent).toHaveBeenCalledWith({
scope: component.scope,
diff --git a/spec/javascripts/error_tracking_settings/components/app_spec.js b/spec/javascripts/error_tracking_settings/components/app_spec.js
new file mode 100644
index 00000000000..2e52a45fd34
--- /dev/null
+++ b/spec/javascripts/error_tracking_settings/components/app_spec.js
@@ -0,0 +1,63 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import ErrorTrackingSettings from '~/error_tracking_settings/components/app.vue';
+import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
+import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
+import createStore from '~/error_tracking_settings/store';
+import { TEST_HOST } from 'spec/test_constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('error tracking settings app', () => {
+ let store;
+ let wrapper;
+
+ function mountComponent() {
+ wrapper = shallowMount(ErrorTrackingSettings, {
+ localVue,
+ store, // Override the imported store
+ propsData: {
+ initialEnabled: 'true',
+ initialApiHost: TEST_HOST,
+ initialToken: 'someToken',
+ initialProject: null,
+ listProjectsEndpoint: TEST_HOST,
+ operationsSettingsEndpoint: TEST_HOST,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ store = createStore();
+
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('section', () => {
+ it('renders the form and dropdown', () => {
+ expect(wrapper.find(ErrorTrackingForm).exists()).toBeTruthy();
+ expect(wrapper.find(ProjectDropdown).exists()).toBeTruthy();
+ });
+
+ it('renders the Save Changes button', () => {
+ expect(wrapper.find('.js-error-tracking-button').exists()).toBeTruthy();
+ });
+
+ it('enables the button by default', () => {
+ expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeFalsy();
+ });
+
+ it('disables the button when saving', () => {
+ store.state.settingsLoading = true;
+
+ expect(wrapper.find('.js-error-tracking-button').attributes('disabled')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js b/spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js
new file mode 100644
index 00000000000..23e57c4bbf1
--- /dev/null
+++ b/spec/javascripts/error_tracking_settings/components/error_tracking_form_spec.js
@@ -0,0 +1,91 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlButton, GlFormInput } from '@gitlab/ui';
+import ErrorTrackingForm from '~/error_tracking_settings/components/error_tracking_form.vue';
+import { defaultProps } from '../mock';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('error tracking settings form', () => {
+ let wrapper;
+
+ function mountComponent() {
+ wrapper = shallowMount(ErrorTrackingForm, {
+ localVue,
+ propsData: defaultProps,
+ });
+ }
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('an empty form', () => {
+ it('is rendered', () => {
+ expect(wrapper.findAll(GlFormInput).length).toBe(2);
+ expect(wrapper.find(GlFormInput).attributes('id')).toBe('error-tracking-api-host');
+ expect(
+ wrapper
+ .findAll(GlFormInput)
+ .at(1)
+ .attributes('id'),
+ ).toBe('error-tracking-token');
+
+ expect(wrapper.findAll(GlButton).exists()).toBe(true);
+ });
+
+ it('is rendered with labels and placeholders', () => {
+ const pageText = wrapper.text();
+
+ expect(pageText).toContain('Find your hostname in your Sentry account settings page');
+ expect(pageText).toContain(
+ "After adding your Auth Token, use the 'Connect' button to load projects",
+ );
+
+ expect(pageText).not.toContain('Connection has failed. Re-check Auth Token and try again');
+ expect(
+ wrapper
+ .findAll(GlFormInput)
+ .at(0)
+ .attributes('placeholder'),
+ ).toContain('https://mysentryserver.com');
+ });
+ });
+
+ describe('after a successful connection', () => {
+ beforeEach(() => {
+ wrapper.setProps({ connectSuccessful: true });
+ });
+
+ it('shows the success checkmark', () => {
+ expect(wrapper.find('.js-error-tracking-connect-success').isVisible()).toBe(true);
+ });
+
+ it('does not show an error', () => {
+ expect(wrapper.text()).not.toContain(
+ 'Connection has failed. Re-check Auth Token and try again',
+ );
+ });
+ });
+
+ describe('after an unsuccessful connection', () => {
+ beforeEach(() => {
+ wrapper.setProps({ connectError: true });
+ });
+
+ it('does not show the check mark', () => {
+ expect(wrapper.find('.js-error-tracking-connect-success').isVisible()).toBe(false);
+ });
+
+ it('shows an error', () => {
+ expect(wrapper.text()).toContain('Connection has failed. Re-check Auth Token and try again');
+ });
+ });
+});
diff --git a/spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js b/spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js
new file mode 100644
index 00000000000..8e5dbe28452
--- /dev/null
+++ b/spec/javascripts/error_tracking_settings/components/project_dropdown_spec.js
@@ -0,0 +1,109 @@
+import _ from 'underscore';
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
+import { defaultProps, projectList, staleProject } from '../mock';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('error tracking settings project dropdown', () => {
+ let wrapper;
+
+ function mountComponent() {
+ wrapper = shallowMount(ProjectDropdown, {
+ localVue,
+ propsData: {
+ ..._.pick(
+ defaultProps,
+ 'dropdownLabel',
+ 'invalidProjectLabel',
+ 'projects',
+ 'projectSelectionLabel',
+ 'selectedProject',
+ 'token',
+ ),
+ hasProjects: false,
+ isProjectInvalid: false,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('empty project list', () => {
+ it('renders the dropdown', () => {
+ expect(wrapper.find('#project-dropdown').exists()).toBeTruthy();
+ expect(wrapper.find(GlDropdown).exists()).toBeTruthy();
+ });
+
+ it('shows helper text', () => {
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBeTruthy();
+ expect(wrapper.find('.js-project-dropdown-label').text()).toContain(
+ 'To enable project selection',
+ );
+ });
+
+ it('does not show an error', () => {
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy();
+ });
+
+ it('does not contain any dropdown items', () => {
+ expect(wrapper.find(GlDropdownItem).exists()).toBeFalsy();
+ expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available');
+ });
+ });
+
+ describe('populated project list', () => {
+ beforeEach(() => {
+ wrapper.setProps({ projects: _.clone(projectList), hasProjects: true });
+ });
+
+ it('renders the dropdown', () => {
+ expect(wrapper.find('#project-dropdown').exists()).toBeTruthy();
+ expect(wrapper.find(GlDropdown).exists()).toBeTruthy();
+ });
+
+ it('contains a number of dropdown items', () => {
+ expect(wrapper.find(GlDropdownItem).exists()).toBeTruthy();
+ expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
+ });
+ });
+
+ describe('selected project', () => {
+ const selectedProject = _.clone(projectList[0]);
+
+ beforeEach(() => {
+ wrapper.setProps({ projects: _.clone(projectList), selectedProject, hasProjects: true });
+ });
+
+ it('does not show helper text', () => {
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy();
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBeFalsy();
+ });
+ });
+
+ describe('invalid project selected', () => {
+ beforeEach(() => {
+ wrapper.setProps({
+ projects: _.clone(projectList),
+ selectedProject: staleProject,
+ isProjectInvalid: true,
+ });
+ });
+
+ it('displays a error', () => {
+ expect(wrapper.find('.js-project-dropdown-label').exists()).toBeFalsy();
+ expect(wrapper.find('.js-project-dropdown-error').exists()).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/javascripts/error_tracking_settings/mock.js b/spec/javascripts/error_tracking_settings/mock.js
new file mode 100644
index 00000000000..32cdba33c14
--- /dev/null
+++ b/spec/javascripts/error_tracking_settings/mock.js
@@ -0,0 +1,92 @@
+import createStore from '~/error_tracking_settings/store';
+import { TEST_HOST } from 'spec/test_constants';
+
+const defaultStore = createStore();
+
+export const projectList = [
+ {
+ name: 'name',
+ slug: 'slug',
+ organizationName: 'organizationName',
+ organizationSlug: 'organizationSlug',
+ },
+ {
+ name: 'name2',
+ slug: 'slug2',
+ organizationName: 'organizationName2',
+ organizationSlug: 'organizationSlug2',
+ },
+];
+
+export const staleProject = {
+ name: 'staleName',
+ slug: 'staleSlug',
+ organizationName: 'staleOrganizationName',
+ organizationSlug: 'staleOrganizationSlug',
+};
+
+export const normalizedProject = {
+ name: 'name',
+ slug: 'slug',
+ organizationName: 'organization_name',
+ organizationSlug: 'organization_slug',
+};
+
+export const sampleBackendProject = {
+ name: normalizedProject.name,
+ slug: normalizedProject.slug,
+ organization_name: normalizedProject.organizationName,
+ organization_slug: normalizedProject.organizationSlug,
+};
+
+export const sampleFrontendSettings = {
+ apiHost: 'apiHost',
+ enabled: false,
+ token: 'token',
+ selectedProject: {
+ slug: normalizedProject.slug,
+ name: normalizedProject.name,
+ organizationName: normalizedProject.organizationName,
+ organizationSlug: normalizedProject.organizationSlug,
+ },
+};
+
+export const transformedSettings = {
+ api_host: 'apiHost',
+ enabled: false,
+ token: 'token',
+ project: {
+ slug: normalizedProject.slug,
+ name: normalizedProject.name,
+ organization_name: normalizedProject.organizationName,
+ organization_slug: normalizedProject.organizationSlug,
+ },
+};
+
+export const defaultProps = {
+ ...defaultStore.state,
+ ...defaultStore.getters,
+};
+
+export const initialEmptyState = {
+ apiHost: '',
+ enabled: false,
+ project: null,
+ token: '',
+ listProjectsEndpoint: TEST_HOST,
+ operationsSettingsEndpoint: TEST_HOST,
+};
+
+export const initialPopulatedState = {
+ apiHost: 'apiHost',
+ enabled: true,
+ project: JSON.stringify(projectList[0]),
+ token: 'token',
+ listProjectsEndpoint: TEST_HOST,
+ operationsSettingsEndpoint: TEST_HOST,
+};
+
+export const projectWithHtmlTemplate = {
+ ...projectList[0],
+ name: '<strong>bold</strong>',
+};
diff --git a/spec/javascripts/error_tracking_settings/store/actions_spec.js b/spec/javascripts/error_tracking_settings/store/actions_spec.js
new file mode 100644
index 00000000000..0255b3a7aa4
--- /dev/null
+++ b/spec/javascripts/error_tracking_settings/store/actions_spec.js
@@ -0,0 +1,191 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { TEST_HOST } from 'spec/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import actionsDefaultExport, * as actions from '~/error_tracking_settings/store/actions';
+import * as types from '~/error_tracking_settings/store/mutation_types';
+import defaultState from '~/error_tracking_settings/store/state';
+import { projectList } from '../mock';
+
+describe('error tracking settings actions', () => {
+ let state;
+
+ describe('project list actions', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = { ...defaultState(), listProjectsEndpoint: TEST_HOST };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should request and transform the project list', done => {
+ mock.onPost(TEST_HOST).reply(() => [200, { projects: projectList }]);
+ testAction(
+ actions.fetchProjects,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestProjects' },
+ {
+ type: 'receiveProjectsSuccess',
+ payload: projectList.map(convertObjectPropsToCamelCase),
+ },
+ ],
+ () => {
+ expect(mock.history.post.length).toBe(1);
+ done();
+ },
+ );
+ });
+
+ it('should handle a server error', done => {
+ mock.onPost(`${TEST_HOST}.json`).reply(() => [400]);
+ testAction(
+ actions.fetchProjects,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestProjects' },
+ {
+ type: 'receiveProjectsError',
+ },
+ ],
+ () => {
+ expect(mock.history.post.length).toBe(1);
+ done();
+ },
+ );
+ });
+
+ it('should request projects correctly', done => {
+ testAction(actions.requestProjects, null, state, [{ type: types.RESET_CONNECT }], [], done);
+ });
+
+ it('should receive projects correctly', done => {
+ const testPayload = [];
+ testAction(
+ actions.receiveProjectsSuccess,
+ testPayload,
+ state,
+ [
+ { type: types.UPDATE_CONNECT_SUCCESS },
+ { type: types.RECEIVE_PROJECTS, payload: testPayload },
+ ],
+ [],
+ done,
+ );
+ });
+
+ it('should handle errors when receiving projects', done => {
+ const testPayload = [];
+ testAction(
+ actions.receiveProjectsError,
+ testPayload,
+ state,
+ [{ type: types.UPDATE_CONNECT_ERROR }, { type: types.CLEAR_PROJECTS }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('save changes actions', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = {
+ operationsSettingsEndpoint: TEST_HOST,
+ };
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should save the page', done => {
+ const refreshCurrentPage = spyOnDependency(actionsDefaultExport, 'refreshCurrentPage');
+ mock.onPatch(TEST_HOST).reply(200);
+ testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }], () => {
+ expect(mock.history.patch.length).toBe(1);
+ expect(refreshCurrentPage).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('should handle a server error', done => {
+ mock.onPatch(TEST_HOST).reply(400);
+ testAction(
+ actions.updateSettings,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestSettings' },
+ {
+ type: 'receiveSettingsError',
+ payload: new Error('Request failed with status code 400'),
+ },
+ ],
+ () => {
+ expect(mock.history.patch.length).toBe(1);
+ done();
+ },
+ );
+ });
+
+ it('should request to save the page', done => {
+ testAction(
+ actions.requestSettings,
+ null,
+ state,
+ [{ type: types.UPDATE_SETTINGS_LOADING, payload: true }],
+ [],
+ done,
+ );
+ });
+
+ it('should handle errors when requesting to save the page', done => {
+ testAction(
+ actions.receiveSettingsError,
+ {},
+ state,
+ [{ type: types.UPDATE_SETTINGS_LOADING, payload: false }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('generic actions to update the store', () => {
+ const testData = 'test';
+ it('should reset the `connect success` flag when updating the api host', done => {
+ testAction(
+ actions.updateApiHost,
+ testData,
+ state,
+ [{ type: types.UPDATE_API_HOST, payload: testData }, { type: types.RESET_CONNECT }],
+ [],
+ done,
+ );
+ });
+
+ it('should reset the `connect success` flag when updating the token', done => {
+ testAction(
+ actions.updateToken,
+ testData,
+ state,
+ [{ type: types.UPDATE_TOKEN, payload: testData }, { type: types.RESET_CONNECT }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/error_tracking_settings/store/getters_spec.js b/spec/javascripts/error_tracking_settings/store/getters_spec.js
new file mode 100644
index 00000000000..2c5ff084b8a
--- /dev/null
+++ b/spec/javascripts/error_tracking_settings/store/getters_spec.js
@@ -0,0 +1,93 @@
+import * as getters from '~/error_tracking_settings/store/getters';
+import defaultState from '~/error_tracking_settings/store/state';
+import { projectList, projectWithHtmlTemplate, staleProject } from '../mock';
+
+describe('Error Tracking Settings - Getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = defaultState();
+ });
+
+ describe('hasProjects', () => {
+ it('should reflect when no projects exist', () => {
+ expect(getters.hasProjects(state)).toEqual(false);
+ });
+
+ it('should reflect when projects exist', () => {
+ state.projects = projectList;
+
+ expect(getters.hasProjects(state)).toEqual(true);
+ });
+ });
+
+ describe('isProjectInvalid', () => {
+ const mockGetters = { hasProjects: true };
+ it('should show when a project is valid', () => {
+ state.projects = projectList;
+ [state.selectedProject] = projectList;
+
+ expect(getters.isProjectInvalid(state, mockGetters)).toEqual(false);
+ });
+
+ it('should show when a project is invalid', () => {
+ state.projects = projectList;
+ state.selectedProject = staleProject;
+
+ expect(getters.isProjectInvalid(state, mockGetters)).toEqual(true);
+ });
+ });
+
+ describe('dropdownLabel', () => {
+ const mockGetters = { hasProjects: false };
+ it('should display correctly when there are no projects available', () => {
+ expect(getters.dropdownLabel(state, mockGetters)).toEqual('No projects available');
+ });
+
+ it('should display correctly when a project is selected', () => {
+ [state.selectedProject] = projectList;
+
+ expect(getters.dropdownLabel(state, mockGetters)).toEqual('organizationName | name');
+ });
+
+ it('should display correctly when no project is selected', () => {
+ state.projects = projectList;
+
+ expect(getters.dropdownLabel(state, { hasProjects: true })).toEqual('Select project');
+ });
+ });
+
+ describe('invalidProjectLabel', () => {
+ it('should display an error containing the project name', () => {
+ [state.selectedProject] = projectList;
+
+ expect(getters.invalidProjectLabel(state)).toEqual(
+ 'Project "name" is no longer available. Select another project to continue.',
+ );
+ });
+
+ it('should properly escape the label text', () => {
+ state.selectedProject = projectWithHtmlTemplate;
+
+ expect(getters.invalidProjectLabel(state)).toEqual(
+ 'Project "&lt;strong&gt;bold&lt;/strong&gt;" is no longer available. Select another project to continue.',
+ );
+ });
+ });
+
+ describe('projectSelectionLabel', () => {
+ it('should show the correct message when the token is empty', () => {
+ expect(getters.projectSelectionLabel(state)).toEqual(
+ 'To enable project selection, enter a valid Auth Token',
+ );
+ });
+
+ it('should show the correct message when token exists', () => {
+ state.token = 'test-token';
+
+ expect(getters.projectSelectionLabel(state)).toEqual(
+ "Click 'Connect' to re-establish the connection to Sentry and activate the dropdown.",
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/error_tracking_settings/store/mutation_spec.js b/spec/javascripts/error_tracking_settings/store/mutation_spec.js
new file mode 100644
index 00000000000..bb1f1da784e
--- /dev/null
+++ b/spec/javascripts/error_tracking_settings/store/mutation_spec.js
@@ -0,0 +1,82 @@
+import { TEST_HOST } from 'spec/test_constants';
+import mutations from '~/error_tracking_settings/store/mutations';
+import defaultState from '~/error_tracking_settings/store/state';
+import * as types from '~/error_tracking_settings/store/mutation_types';
+import {
+ initialEmptyState,
+ initialPopulatedState,
+ projectList,
+ sampleBackendProject,
+ normalizedProject,
+} from '../mock';
+
+describe('error tracking settings mutations', () => {
+ describe('mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = defaultState();
+ });
+
+ it('should create an empty initial state correctly', () => {
+ mutations[types.SET_INITIAL_STATE](state, {
+ ...initialEmptyState,
+ });
+
+ expect(state.apiHost).toEqual('');
+ expect(state.enabled).toEqual(false);
+ expect(state.selectedProject).toEqual(null);
+ expect(state.token).toEqual('');
+ expect(state.listProjectsEndpoint).toEqual(TEST_HOST);
+ expect(state.operationsSettingsEndpoint).toEqual(TEST_HOST);
+ });
+
+ it('should populate the initial state correctly', () => {
+ mutations[types.SET_INITIAL_STATE](state, {
+ ...initialPopulatedState,
+ });
+
+ expect(state.apiHost).toEqual('apiHost');
+ expect(state.enabled).toEqual(true);
+ expect(state.selectedProject).toEqual(projectList[0]);
+ expect(state.token).toEqual('token');
+ expect(state.listProjectsEndpoint).toEqual(TEST_HOST);
+ expect(state.operationsSettingsEndpoint).toEqual(TEST_HOST);
+ });
+
+ it('should receive projects successfully', () => {
+ mutations[types.RECEIVE_PROJECTS](state, [sampleBackendProject]);
+
+ expect(state.projects).toEqual([normalizedProject]);
+ });
+
+ it('should strip out unnecessary project properties', () => {
+ mutations[types.RECEIVE_PROJECTS](state, [
+ { ...sampleBackendProject, extra_property: 'extra_property' },
+ ]);
+
+ expect(state.projects).toEqual([normalizedProject]);
+ });
+
+ it('should update state when connect is successful', () => {
+ mutations[types.UPDATE_CONNECT_SUCCESS](state);
+
+ expect(state.connectSuccessful).toBe(true);
+ expect(state.connectError).toBe(false);
+ });
+
+ it('should update state when connect fails', () => {
+ mutations[types.UPDATE_CONNECT_ERROR](state);
+
+ expect(state.connectSuccessful).toBe(false);
+ expect(state.connectError).toBe(true);
+ });
+
+ it('should update state when connect is reset', () => {
+ mutations[types.RESET_CONNECT](state);
+
+ expect(state.connectSuccessful).toBe(false);
+ expect(state.connectError).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/error_tracking_settings/utils_spec.js b/spec/javascripts/error_tracking_settings/utils_spec.js
new file mode 100644
index 00000000000..4b144f7daf1
--- /dev/null
+++ b/spec/javascripts/error_tracking_settings/utils_spec.js
@@ -0,0 +1,29 @@
+import { transformFrontendSettings } from '~/error_tracking_settings/utils';
+import { sampleFrontendSettings, transformedSettings } from './mock';
+
+describe('error tracking settings utils', () => {
+ describe('data transform functions', () => {
+ it('should transform settings successfully for the backend', () => {
+ expect(transformFrontendSettings(sampleFrontendSettings)).toEqual(transformedSettings);
+ });
+
+ it('should transform empty values in the settings object to null', () => {
+ const emptyFrontendSettingsObject = {
+ apiHost: '',
+ enabled: false,
+ token: '',
+ selectedProject: null,
+ };
+ const transformedEmptySettingsObject = {
+ api_host: null,
+ enabled: false,
+ token: null,
+ project: null,
+ };
+
+ expect(transformFrontendSettings(emptyFrontendSettingsObject)).toEqual(
+ transformedEmptySettingsObject,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js
index e8fcc8592eb..f764800fff0 100644
--- a/spec/javascripts/filtered_search/dropdown_user_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_user_spec.js
@@ -72,7 +72,7 @@ describe('Dropdown User', () => {
});
describe('hideCurrentUser', () => {
- const fixtureTemplate = 'issues/issue_list.html.raw';
+ const fixtureTemplate = 'issues/issue_list.html';
preloadFixtures(fixtureTemplate);
let dropdown;
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index cfd0b96ec43..62d1bd69635 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -4,7 +4,7 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Dropdown Utils', () => {
- const issueListFixture = 'issues/issue_list.html.raw';
+ const issueListFixture = 'issues/issue_list.html';
preloadFixtures(issueListFixture);
describe('getEscapedText', () => {
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index 6230da77f49..a72ea6ab547 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -1,9 +1,4 @@
-import _ from 'underscore';
-import AjaxCache from '~/lib/utils/ajax_cache';
-import UsersCache from '~/lib/utils/users_cache';
-
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
-import DropdownUtils from '~/filtered_search//dropdown_utils';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Filtered Search Visual Tokens', () => {
@@ -298,6 +293,7 @@ describe('Filtered Search Visual Tokens', () => {
subject.addVisualTokenElement('milestone');
const token = tokensContainer.querySelector('.js-visual-token');
+ expect(token.classList.contains('search-token-milestone')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('milestone');
expect(token.querySelector('.value')).toEqual(null);
@@ -307,6 +303,7 @@ describe('Filtered Search Visual Tokens', () => {
subject.addVisualTokenElement('label', 'Frontend');
const token = tokensContainer.querySelector('.js-visual-token');
+ expect(token.classList.contains('search-token-label')).toEqual(true);
expect(token.classList.contains('filtered-search-token')).toEqual(true);
expect(token.querySelector('.name').innerText).toEqual('label');
expect(token.querySelector('.value').innerText).toEqual('Frontend');
@@ -322,10 +319,12 @@ describe('Filtered Search Visual Tokens', () => {
const labelToken = tokens[0];
const assigneeToken = tokens[1];
+ expect(labelToken.classList.contains('search-token-label')).toEqual(true);
expect(labelToken.classList.contains('filtered-search-token')).toEqual(true);
expect(labelToken.querySelector('.name').innerText).toEqual('label');
expect(labelToken.querySelector('.value').innerText).toEqual('Frontend');
+ expect(assigneeToken.classList.contains('search-token-assignee')).toEqual(true);
expect(assigneeToken.classList.contains('filtered-search-token')).toEqual(true);
expect(assigneeToken.querySelector('.name').innerText).toEqual('assignee');
expect(assigneeToken.querySelector('.value').innerText).toEqual('@root');
@@ -685,349 +684,21 @@ describe('Filtered Search Visual Tokens', () => {
});
describe('renderVisualTokenValue', () => {
- const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search');
- const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken(
- 'milestone',
- 'upcoming',
- );
-
- let updateLabelTokenColorSpy;
- let updateUserTokenAppearanceSpy;
-
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${authorToken.outerHTML}
${bugLabelToken.outerHTML}
- ${keywordToken.outerHTML}
- ${milestoneToken.outerHTML}
`);
-
- spyOn(subject, 'updateLabelTokenColor');
- updateLabelTokenColorSpy = subject.updateLabelTokenColor;
-
- spyOn(subject, 'updateUserTokenAppearance');
- updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance;
});
it('renders a author token value element', () => {
- const { tokenNameElement, tokenValueContainer, tokenValueElement } = findElements(
- authorToken,
- );
+ const { tokenNameElement, tokenValueElement } = findElements(authorToken);
const tokenName = tokenNameElement.innerText;
const tokenValue = 'new value';
subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
expect(tokenValueElement.innerText).toBe(tokenValue);
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1);
- const expectedArgs = [tokenValueContainer, tokenValueElement, tokenValue];
-
- expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs);
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- });
-
- it('renders a label token value element', () => {
- const { tokenNameElement, tokenValueContainer, tokenValueElement } = findElements(
- bugLabelToken,
- );
- const tokenName = tokenNameElement.innerText;
- const tokenValue = 'new value';
-
- subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue);
-
- expect(tokenValueElement.innerText).toBe(tokenValue);
- expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
- const expectedArgs = [tokenValueContainer, tokenValue];
-
- expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('renders a milestone token value element', () => {
- const { tokenNameElement, tokenValueElement } = findElements(milestoneToken);
- const tokenName = tokenNameElement.innerText;
- const tokenValue = 'new value';
-
- subject.renderVisualTokenValue(milestoneToken, tokenName, tokenValue);
-
- expect(tokenValueElement.innerText).toBe(tokenValue);
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('does not update user token appearance for `None` filter', () => {
- const { tokenNameElement } = findElements(authorToken);
-
- const tokenName = tokenNameElement.innerText;
- const tokenValue = 'None';
-
- subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
-
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('does not update user token appearance for `none` filter', () => {
- const { tokenNameElement } = findElements(authorToken);
-
- const tokenName = tokenNameElement.innerText;
- const tokenValue = 'none';
-
- subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
-
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('does not update user token appearance for `any` filter', () => {
- const { tokenNameElement } = findElements(authorToken);
-
- const tokenName = tokenNameElement.innerText;
- const tokenValue = 'any';
-
- subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
-
- expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
- });
-
- it('does not update label token color for `none` filter', () => {
- const { tokenNameElement } = findElements(bugLabelToken);
-
- const tokenName = tokenNameElement.innerText;
- const tokenValue = 'none';
-
- subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue);
-
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- });
-
- it('does not update label token color for `any` filter', () => {
- const { tokenNameElement } = findElements(bugLabelToken);
-
- const tokenName = tokenNameElement.innerText;
- const tokenValue = 'any';
-
- subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue);
-
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- });
- });
-
- describe('updateUserTokenAppearance', () => {
- let usersCacheSpy;
-
- beforeEach(() => {
- spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username));
- });
-
- it('ignores error if UsersCache throws', done => {
- spyOn(window, 'Flash');
- const dummyError = new Error('Earth rotated backwards');
- const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
- const tokenValue = tokenValueElement.innerText;
- usersCacheSpy = username => {
- expect(`@${username}`).toBe(tokenValue);
- return Promise.reject(dummyError);
- };
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(window.Flash.calls.count()).toBe(0);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does nothing if user cannot be found', done => {
- const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
- const tokenValue = tokenValueElement.innerText;
- usersCacheSpy = username => {
- expect(`@${username}`).toBe(tokenValue);
- return Promise.resolve(undefined);
- };
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueElement.innerText).toBe(tokenValue);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('replaces author token with avatar and display name', done => {
- const dummyUser = {
- name: 'Important Person',
- avatar_url: 'https://host.invalid/mypics/avatar.png',
- };
- const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
- const tokenValue = tokenValueElement.innerText;
- usersCacheSpy = username => {
- expect(`@${username}`).toBe(tokenValue);
- return Promise.resolve(dummyUser);
- };
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
- expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
- const avatar = tokenValueElement.querySelector('img.avatar');
-
- expect(avatar.src).toBe(dummyUser.avatar_url);
- expect(avatar.alt).toBe('');
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('escapes user name when creating token', done => {
- const dummyUser = {
- name: '<script>',
- avatar_url: `${gl.TEST_HOST}/mypics/avatar.png`,
- };
- const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
- const tokenValue = tokenValueElement.innerText;
- usersCacheSpy = username => {
- expect(`@${username}`).toBe(tokenValue);
- return Promise.resolve(dummyUser);
- };
-
- subject
- .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
- .then(() => {
- expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
- tokenValueElement.querySelector('.avatar').remove();
-
- expect(tokenValueElement.innerHTML.trim()).toBe(_.escape(dummyUser.name));
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('setTokenStyle', () => {
- let originalTextColor;
-
- beforeEach(() => {
- originalTextColor = bugLabelToken.style.color;
- });
-
- it('should set backgroundColor', () => {
- const originalBackgroundColor = bugLabelToken.style.backgroundColor;
- const token = subject.setTokenStyle(bugLabelToken, 'blue', 'white');
-
- expect(token.style.backgroundColor).toEqual('blue');
- expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor);
- });
-
- it('should set textColor', () => {
- const token = subject.setTokenStyle(bugLabelToken, 'white', 'black');
-
- expect(token.style.color).toEqual('black');
- expect(token.style.color).not.toEqual(originalTextColor);
- });
-
- it('should add inverted class when textColor is #FFFFFF', () => {
- const token = subject.setTokenStyle(bugLabelToken, 'black', '#FFFFFF');
-
- expect(token.style.color).toEqual('rgb(255, 255, 255)');
- expect(token.style.color).not.toEqual(originalTextColor);
- expect(token.querySelector('.remove-token').classList.contains('inverted')).toEqual(true);
- });
- });
-
- describe('updateLabelTokenColor', () => {
- const jsonFixtureName = 'labels/project_labels.json';
- const dummyEndpoint = '/dummy/endpoint';
-
- preloadFixtures(jsonFixtureName);
-
- let labelData;
-
- beforeAll(() => {
- labelData = getJSONFixture(jsonFixtureName);
- });
-
- const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
- 'label',
- '~doesnotexist',
- );
- const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
- 'label',
- '~"some space"',
- );
-
- beforeEach(() => {
- tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${bugLabelToken.outerHTML}
- ${missingLabelToken.outerHTML}
- ${spaceLabelToken.outerHTML}
- `);
-
- const filteredSearchInput = document.querySelector('.filtered-search');
- filteredSearchInput.dataset.baseEndpoint = dummyEndpoint;
-
- AjaxCache.internalStorage = {};
- AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData;
- });
-
- const parseColor = color => {
- const dummyElement = document.createElement('div');
- dummyElement.style.color = color;
- return dummyElement.style.color;
- };
-
- const expectValueContainerStyle = (tokenValueContainer, label) => {
- expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
- expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
- expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
- };
-
- const findLabel = tokenValue =>
- labelData.find(label => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`);
-
- it('updates the color of a label token', done => {
- const { tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
- const tokenValue = tokenValueElement.innerText;
- const matchingLabel = findLabel(tokenValue);
-
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expectValueContainerStyle(tokenValueContainer, matchingLabel);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('updates the color of a label token with spaces', done => {
- const { tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken);
- const tokenValue = tokenValueElement.innerText;
- const matchingLabel = findLabel(tokenValue);
-
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expectValueContainerStyle(tokenValueContainer, matchingLabel);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not change color of a missing label', done => {
- const { tokenValueContainer, tokenValueElement } = findElements(missingLabelToken);
- const tokenValue = tokenValueElement.innerText;
- const matchingLabel = findLabel(tokenValue);
-
- expect(matchingLabel).toBe(undefined);
-
- subject
- .updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- expect(tokenValueContainer.getAttribute('style')).toBe(null);
- })
- .then(done)
- .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/filtered_search/visual_token_value_spec.js b/spec/javascripts/filtered_search/visual_token_value_spec.js
new file mode 100644
index 00000000000..14217d460cc
--- /dev/null
+++ b/spec/javascripts/filtered_search/visual_token_value_spec.js
@@ -0,0 +1,383 @@
+import VisualTokenValue from '~/filtered_search/visual_token_value';
+import _ from 'underscore';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import UsersCache from '~/lib/utils/users_cache';
+import DropdownUtils from '~/filtered_search//dropdown_utils';
+import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
+
+describe('Filtered Search Visual Tokens', () => {
+ const findElements = tokenElement => {
+ const tokenNameElement = tokenElement.querySelector('.name');
+ const tokenValueContainer = tokenElement.querySelector('.value-container');
+ const tokenValueElement = tokenValueContainer.querySelector('.value');
+ const tokenType = tokenNameElement.innerText.toLowerCase();
+ const tokenValue = tokenValueElement.innerText;
+ const subject = new VisualTokenValue(tokenValue, tokenType);
+ return { subject, tokenValueContainer, tokenValueElement };
+ };
+
+ let tokensContainer;
+ let authorToken;
+ let bugLabelToken;
+
+ beforeEach(() => {
+ setFixtures(`
+ <ul class="tokens-container">
+ ${FilteredSearchSpecHelper.createInputHTML()}
+ </ul>
+ `);
+ tokensContainer = document.querySelector('.tokens-container');
+
+ authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user');
+ bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug');
+ });
+
+ describe('updateUserTokenAppearance', () => {
+ let usersCacheSpy;
+
+ beforeEach(() => {
+ spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username));
+ });
+
+ it('ignores error if UsersCache throws', done => {
+ spyOn(window, 'Flash');
+ const dummyError = new Error('Earth rotated backwards');
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = username => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.reject(dummyError);
+ };
+
+ subject
+ .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(window.Flash.calls.count()).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does nothing if user cannot be found', done => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = username => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(undefined);
+ };
+
+ subject
+ .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('replaces author token with avatar and display name', done => {
+ const dummyUser = {
+ name: 'Important Person',
+ avatar_url: 'https://host.invalid/mypics/avatar.png',
+ };
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = username => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(dummyUser);
+ };
+
+ subject
+ .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ const avatar = tokenValueElement.querySelector('img.avatar');
+
+ expect(avatar.src).toBe(dummyUser.avatar_url);
+ expect(avatar.alt).toBe('');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('escapes user name when creating token', done => {
+ const dummyUser = {
+ name: '<script>',
+ avatar_url: `${gl.TEST_HOST}/mypics/avatar.png`,
+ };
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = username => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(dummyUser);
+ };
+
+ subject
+ .updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ tokenValueElement.querySelector('.avatar').remove();
+
+ expect(tokenValueElement.innerHTML.trim()).toBe(_.escape(dummyUser.name));
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('updateLabelTokenColor', () => {
+ const jsonFixtureName = 'labels/project_labels.json';
+ const dummyEndpoint = '/dummy/endpoint';
+
+ preloadFixtures(jsonFixtureName);
+
+ let labelData;
+
+ beforeAll(() => {
+ labelData = getJSONFixture(jsonFixtureName);
+ });
+
+ const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
+ 'label',
+ '~doesnotexist',
+ );
+ const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken(
+ 'label',
+ '~"some space"',
+ );
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${bugLabelToken.outerHTML}
+ ${missingLabelToken.outerHTML}
+ ${spaceLabelToken.outerHTML}
+ `);
+
+ const filteredSearchInput = document.querySelector('.filtered-search');
+ filteredSearchInput.dataset.baseEndpoint = dummyEndpoint;
+
+ AjaxCache.internalStorage = {};
+ AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData;
+ });
+
+ const parseColor = color => {
+ const dummyElement = document.createElement('div');
+ dummyElement.style.color = color;
+ return dummyElement.style.color;
+ };
+
+ const expectValueContainerStyle = (tokenValueContainer, label) => {
+ expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
+ expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
+ expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
+ };
+
+ const findLabel = tokenValue =>
+ labelData.find(label => tokenValue === `~${DropdownUtils.getEscapedText(label.title)}`);
+
+ it('updates the color of a label token', done => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ subject
+ .updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates the color of a label token with spaces', done => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ subject
+ .updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not change color of a missing label', done => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(missingLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ expect(matchingLabel).toBe(undefined);
+
+ subject
+ .updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.getAttribute('style')).toBe(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('setTokenStyle', () => {
+ let originalTextColor;
+
+ beforeEach(() => {
+ originalTextColor = bugLabelToken.style.color;
+ });
+
+ it('should set backgroundColor', () => {
+ const originalBackgroundColor = bugLabelToken.style.backgroundColor;
+ const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'blue', 'white');
+
+ expect(token.style.backgroundColor).toEqual('blue');
+ expect(token.style.backgroundColor).not.toEqual(originalBackgroundColor);
+ });
+
+ it('should set textColor', () => {
+ const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'white', 'black');
+
+ expect(token.style.color).toEqual('black');
+ expect(token.style.color).not.toEqual(originalTextColor);
+ });
+
+ it('should add inverted class when textColor is #FFFFFF', () => {
+ const token = VisualTokenValue.setTokenStyle(bugLabelToken, 'black', '#FFFFFF');
+
+ expect(token.style.color).toEqual('rgb(255, 255, 255)');
+ expect(token.style.color).not.toEqual(originalTextColor);
+ expect(token.querySelector('.remove-token').classList.contains('inverted')).toEqual(true);
+ });
+ });
+
+ describe('render', () => {
+ const setupSpies = subject => {
+ spyOn(subject, 'updateLabelTokenColor'); // eslint-disable-line jasmine/no-unsafe-spy
+ const updateLabelTokenColorSpy = subject.updateLabelTokenColor;
+
+ spyOn(subject, 'updateUserTokenAppearance'); // eslint-disable-line jasmine/no-unsafe-spy
+ const updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance;
+
+ return { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy };
+ };
+
+ const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search');
+ const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken(
+ 'milestone',
+ 'upcoming',
+ );
+
+ beforeEach(() => {
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
+ ${authorToken.outerHTML}
+ ${bugLabelToken.outerHTML}
+ ${keywordToken.outerHTML}
+ ${milestoneToken.outerHTML}
+ `);
+ });
+
+ it('renders a author token value element', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1);
+ const expectedArgs = [tokenValueContainer, tokenValueElement];
+
+ expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ });
+
+ it('renders a label token value element', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+
+ const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
+ const expectedArgs = [tokenValueContainer];
+
+ expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('renders a milestone token value element', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(milestoneToken);
+
+ const { updateLabelTokenColorSpy, updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update user token appearance for `none` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.tokenValue = 'none';
+
+ const { updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update user token appearance for `None` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.tokenValue = 'None';
+
+ const { updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update user token appearance for `any` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.tokenValue = 'any';
+
+ const { updateUserTokenAppearanceSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update label token color for `None` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+
+ subject.tokenValue = 'None';
+
+ const { updateLabelTokenColorSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update label token color for `none` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+
+ subject.tokenValue = 'none';
+
+ const { updateLabelTokenColorSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ });
+
+ it('does not update label token color for `any` filter', () => {
+ const { subject, tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+
+ subject.tokenValue = 'any';
+
+ const { updateLabelTokenColorSpy } = setupSpies(subject);
+ subject.render(tokenValueContainer, tokenValueElement);
+
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore
index 0c35cdd778e..bed020f5b0a 100644
--- a/spec/javascripts/fixtures/.gitignore
+++ b/spec/javascripts/fixtures/.gitignore
@@ -1,2 +1,5 @@
*.html.raw
+*.html
*.json
+*.pdf
+*.bmpr
diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb
index 387858cba77..e0aaecf626a 100644
--- a/spec/javascripts/fixtures/abuse_reports.rb
+++ b/spec/javascripts/fixtures/abuse_reports.rb
@@ -18,10 +18,9 @@ describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controll
sign_in(admin)
end
- it 'abuse_reports/abuse_reports_list.html.raw' do |example|
+ it 'abuse_reports/abuse_reports_list.html' do
get :index
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/admin_users.rb b/spec/javascripts/fixtures/admin_users.rb
index 9989ac4fff2..22a5de66577 100644
--- a/spec/javascripts/fixtures/admin_users.rb
+++ b/spec/javascripts/fixtures/admin_users.rb
@@ -17,13 +17,12 @@ describe Admin::UsersController, '(JavaScript fixtures)', type: :controller do
clean_frontend_fixtures('admin/users')
end
- it 'admin/users/new_with_internal_user_regex.html.raw' do |example|
+ it 'admin/users/new_with_internal_user_regex.html' do
stub_application_setting(user_default_external: true)
stub_application_setting(user_default_internal_regex: '^(?:(?!\.ext@).)*$\r?')
get :new
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml b/spec/javascripts/fixtures/ajax_loading_spinner.html.haml
deleted file mode 100644
index 09d8c9df3b2..00000000000
--- a/spec/javascripts/fixtures/ajax_loading_spinner.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-%a.js-ajax-loading-spinner{href: "http://goesnowhere.nothing/whereami", data: {remote: true}}
- %i.fa.fa-trash-o
diff --git a/spec/javascripts/fixtures/application_settings.rb b/spec/javascripts/fixtures/application_settings.rb
index a9d3043f73d..d4651fa6ece 100644
--- a/spec/javascripts/fixtures/application_settings.rb
+++ b/spec/javascripts/fixtures/application_settings.rb
@@ -23,12 +23,11 @@ describe Admin::ApplicationSettingsController, '(JavaScript fixtures)', type: :c
remove_repository(project)
end
- it 'application_settings/accounts_and_limit.html.raw' do |example|
+ it 'application_settings/accounts_and_limit.html' do
stub_application_setting(user_default_external: false)
get :show
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/autocomplete_sources.rb b/spec/javascripts/fixtures/autocomplete_sources.rb
new file mode 100644
index 00000000000..b20a0159d7d
--- /dev/null
+++ b/spec/javascripts/fixtures/autocomplete_sources.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ set(:admin) { create(:admin) }
+ set(:group) { create(:group, name: 'frontend-fixtures') }
+ set(:project) { create(:project, namespace: group, path: 'autocomplete-sources-project') }
+ set(:issue) { create(:issue, project: project) }
+
+ before(:all) do
+ clean_frontend_fixtures('autocomplete_sources/')
+ end
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'autocomplete_sources/labels.json' do
+ issue.labels << create(:label, project: project, title: 'bug')
+ issue.labels << create(:label, project: project, title: 'critical')
+
+ create(:label, project: project, title: 'feature')
+ create(:label, project: project, title: 'documentation')
+
+ get :labels,
+ format: :json,
+ params: {
+ namespace_id: group.path,
+ project_id: project.path,
+ type: issue.class.name,
+ type_id: issue.id
+ }
+
+ expect(response).to be_success
+ end
+end
diff --git a/spec/javascripts/fixtures/balsamiq.rb b/spec/javascripts/fixtures/balsamiq.rb
deleted file mode 100644
index 234e246119a..00000000000
--- a/spec/javascripts/fixtures/balsamiq.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-
-describe 'Balsamiq file', '(JavaScript fixtures)', type: :controller do
- include JavaScriptFixturesHelpers
-
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project, :repository, namespace: namespace, path: 'balsamiq-project') }
-
- before(:all) do
- clean_frontend_fixtures('blob/balsamiq/')
- end
-
- it 'blob/balsamiq/test.bmpr' do |example|
- blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr')
-
- store_frontend_fixture(blob.data.force_encoding('utf-8'), example.description)
- end
-end
diff --git a/spec/javascripts/fixtures/balsamiq_viewer.html.haml b/spec/javascripts/fixtures/balsamiq_viewer.html.haml
deleted file mode 100644
index 18166ba4901..00000000000
--- a/spec/javascripts/fixtures/balsamiq_viewer.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: '/test' } }
diff --git a/spec/javascripts/fixtures/blob.rb b/spec/javascripts/fixtures/blob.rb
index cd66d98f92a..07670552cd5 100644
--- a/spec/javascripts/fixtures/blob.rb
+++ b/spec/javascripts/fixtures/blob.rb
@@ -22,7 +22,7 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
remove_repository(project)
end
- it 'blob/show.html.raw' do |example|
+ it 'blob/show.html' do
get(:show, params: {
namespace_id: project.namespace,
project_id: project,
@@ -30,6 +30,5 @@ describe Projects::BlobController, '(JavaScript fixtures)', type: :controller do
})
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/boards.rb b/spec/javascripts/fixtures/boards.rb
index 1d675e008ba..5835721d3d5 100644
--- a/spec/javascripts/fixtures/boards.rb
+++ b/spec/javascripts/fixtures/boards.rb
@@ -17,13 +17,12 @@ describe Projects::BoardsController, '(JavaScript fixtures)', type: :controller
sign_in(admin)
end
- it 'boards/show.html.raw' do |example|
+ it 'boards/show.html' do
get(:index, params: {
namespace_id: project.namespace,
project_id: project
})
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/branches.rb b/spec/javascripts/fixtures/branches.rb
index 3cc713ef90f..204aa9b7c7a 100644
--- a/spec/javascripts/fixtures/branches.rb
+++ b/spec/javascripts/fixtures/branches.rb
@@ -21,13 +21,12 @@ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controlle
remove_repository(project)
end
- it 'branches/new_branch.html.raw' do |example|
+ it 'branches/new_branch.html' do
get :new, params: {
namespace_id: project.namespace.to_param,
project_id: project
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index 69dbe54ffc2..1076404e0e3 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -22,7 +22,7 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
remove_repository(project)
end
- it 'clusters/show_cluster.html.raw' do |example|
+ it 'clusters/show_cluster.html' do
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -30,6 +30,5 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/commit.rb b/spec/javascripts/fixtures/commit.rb
index 295f13b34a4..ff9a4bc1adc 100644
--- a/spec/javascripts/fixtures/commit.rb
+++ b/spec/javascripts/fixtures/commit.rb
@@ -19,7 +19,7 @@ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
- it 'commit/show.html.raw' do |example|
+ it 'commit/show.html' do
params = {
namespace_id: project.namespace,
project_id: project,
@@ -29,6 +29,5 @@ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller
get :show, params: params
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/create_item_dropdown.html.haml b/spec/javascripts/fixtures/create_item_dropdown.html.haml
deleted file mode 100644
index d4d91b93caf..00000000000
--- a/spec/javascripts/fixtures/create_item_dropdown.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-.js-create-item-dropdown-fixture-root
- %input{ name: 'variable[environment]', type: 'hidden' }
- = dropdown_tag('some label',
- options: { toggle_class: 'js-dropdown-menu-toggle',
- content_class: 'js-dropdown-content',
- filter: true,
- dropdown_class: "dropdown-menu-selectable",
- footer_content: true }) do
- %ul.dropdown-footer-list
- %li
- %button{ class: "dropdown-create-new-item-button js-dropdown-create-new-item" }
- Create wildcard
- %code
diff --git a/spec/javascripts/fixtures/deploy_keys.rb b/spec/javascripts/fixtures/deploy_keys.rb
index a333d9c0150..38eab853da2 100644
--- a/spec/javascripts/fixtures/deploy_keys.rb
+++ b/spec/javascripts/fixtures/deploy_keys.rb
@@ -24,7 +24,7 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
render_views
- it 'deploy_keys/keys.json' do |example|
+ it 'deploy_keys/keys.json' do
create(:rsa_deploy_key_2048, public: true)
project_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCdMHEHyhRjbhEZVddFn6lTWdgEy5Q6Bz4nwGB76xWZI5YT/1WJOMEW+sL5zYd31kk7sd3FJ5L9ft8zWMWrr/iWXQikC2cqZK24H1xy+ZUmrRuJD4qGAaIVoyyzBL+avL+lF8J5lg6YSw8gwJY/lX64/vnJHUlWw2n5BF8IFOWhiw== dummy@gitlab.com')
internal_key = create(:deploy_key, key: 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDNd/UJWhPrpb+b/G5oL109y57yKuCxE+WUGJGYaj7WQKsYRJmLYh1mgjrl+KVyfsWpq4ylOxIfFSnN9xBBFN8mlb0Fma5DC7YsSsibJr3MZ19ZNBprwNcdogET7aW9I0In7Wu5f2KqI6e5W/spJHCy4JVxzVMUvk6Myab0LnJ2iQ== dummy@gitlab.com')
@@ -39,6 +39,5 @@ describe Projects::DeployKeysController, '(JavaScript fixtures)', type: :control
}, format: :json
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/environments/table.html.haml b/spec/javascripts/fixtures/environments/table.html.haml
deleted file mode 100644
index 59edc0396d2..00000000000
--- a/spec/javascripts/fixtures/environments/table.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-%table
- %thead
- %tr
- %th Environment
- %th Last deployment
- %th Job
- %th Commit
- %th
- %th
- %tbody
- %tr#environment-row
diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml
deleted file mode 100644
index aa7af61c7eb..00000000000
--- a/spec/javascripts/fixtures/event_filter.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
- %li.active
- %a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"}
- %span
- All
- %li
- %a.event-filter-link{ id: "push_event_filter", title: "Filter by push events", href: "/dashboard/activity"}
- %span
- Push events
- %li
- %a.event-filter-link{ id: "merged_event_filter", title: "Filter by merge events", href: "/dashboard/activity"}
- %span
- Merge events
- %li
- %a.event-filter-link{ id: "issue_event_filter", title: "Filter by issue events", href: "/dashboard/activity"}
- %span
- Issue events
- %li
- %a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"}
- %span
- Comments
- %li
- %a.event-filter-link{ id: "team_event_filter", title: "Filter by team", href: "/dashboard/activity"}
- %span
- Team \ No newline at end of file
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
deleted file mode 100644
index 43d57c2c4dc..00000000000
--- a/spec/javascripts/fixtures/gl_dropdown.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-%div
- .dropdown.inline
- %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
- .dropdown-toggle-text
- Projects
- %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
- .dropdown-menu.dropdown-select.dropdown-menu-selectable
- .dropdown-title
- %span Go to project
- %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
- %i.fa.fa-times.dropdown-menu-close-icon
- .dropdown-input
- %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
- %i.fa.fa-search.dropdown-input-search
- .dropdown-content
- .dropdown-loading
- %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/gl_field_errors.html.haml b/spec/javascripts/fixtures/gl_field_errors.html.haml
deleted file mode 100644
index 69445b61367..00000000000
--- a/spec/javascripts/fixtures/gl_field_errors.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-%form.gl-show-field-errors{action: 'submit', method: 'post'}
- .form-group
- %input.required-text{required: true, type: 'text'} Text
- .form-group
- %input.email{type: 'email', title: 'Please provide a valid email address.', required: true } Email
- .form-group
- %input.password{type: 'password', required: true} Password
- .form-group
- %input.alphanumeric{type: 'text', pattern: '[a-zA-Z0-9]', required: true} Alphanumeric
- .form-group
- %input.hidden{ type:'hidden' }
- .form-group
- %input.custom.gl-field-error-ignore{ type:'text' } Custom, do not validate
- .form-group
- %input.submit{type: 'submit'} Submit
diff --git a/spec/javascripts/fixtures/groups.rb b/spec/javascripts/fixtures/groups.rb
index 03136f4e661..4d0afc3ce1a 100644
--- a/spec/javascripts/fixtures/groups.rb
+++ b/spec/javascripts/fixtures/groups.rb
@@ -18,20 +18,18 @@ describe 'Groups (JavaScript fixtures)', type: :controller do
end
describe GroupsController, '(JavaScript fixtures)', type: :controller do
- it 'groups/edit.html.raw' do |example|
+ it 'groups/edit.html' do
get :edit, params: { id: group }
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
describe Groups::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
- it 'groups/ci_cd_settings.html.raw' do |example|
+ it 'groups/ci_cd_settings.html' do
get :show, params: { group_id: group }
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
end
diff --git a/spec/javascripts/fixtures/issuable_filter.html.haml b/spec/javascripts/fixtures/issuable_filter.html.haml
deleted file mode 100644
index 84fa5395cb8..00000000000
--- a/spec/javascripts/fixtures/issuable_filter.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-%form.js-filter-form{action: '/user/project/issues?scope=all&state=closed'}
- %input{id: 'utf8', name: 'utf8', value: '✓'}
- %input{id: 'check-all-issues', name: 'check-all-issues'}
- %input{id: 'search', name: 'search'}
- %input{id: 'author_id', name: 'author_id'}
- %input{id: 'assignee_id', name: 'assignee_id'}
- %input{id: 'milestone_title', name: 'milestone_title'}
- %input{id: 'label_name', name: 'label_name'}
diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
deleted file mode 100644
index 06ce248dc9c..00000000000
--- a/spec/javascripts/fixtures/issue_sidebar_label.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.block.labels
- .sidebar-collapsed-icon.js-sidebar-labels-tooltip
- .title.hide-collapsed
- %a.edit-link.float-right{ href: "#" }
- Edit
- .selectbox.hide-collapsed{ style: "display: none;" }
- .dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } }
- %span.dropdown-toggle-text
- Label
- %i.fa.fa-chevron-down
- .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
- .dropdown-page-one
- .dropdown-content
- .dropdown-loading
- %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 9b8e90c2a43..d8d77f767de 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -21,26 +21,26 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
remove_repository(project)
end
- it 'issues/open-issue.html.raw' do |example|
- render_issue(example.description, create(:issue, project: project))
+ it 'issues/open-issue.html' do
+ render_issue(create(:issue, project: project))
end
- it 'issues/closed-issue.html.raw' do |example|
- render_issue(example.description, create(:closed_issue, project: project))
+ it 'issues/closed-issue.html' do
+ render_issue(create(:closed_issue, project: project))
end
- it 'issues/issue-with-task-list.html.raw' do |example|
+ it 'issues/issue-with-task-list.html' do
issue = create(:issue, project: project, description: '- [ ] Task List Item')
- render_issue(example.description, issue)
+ render_issue(issue)
end
- it 'issues/issue_with_comment.html.raw' do |example|
+ it 'issues/issue_with_comment.html' do
issue = create(:issue, project: project)
create(:note, project: project, noteable: issue, note: '- [ ] Task List Item').save
- render_issue(example.description, issue)
+ render_issue(issue)
end
- it 'issues/issue_list.html.raw' do |example|
+ it 'issues/issue_list.html' do
create(:issue, project: project)
get :index, params: {
@@ -49,12 +49,11 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
private
- def render_issue(fixture_file_name, issue)
+ def render_issue(issue)
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -62,6 +61,62 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
}
expect(response).to be_success
- store_frontend_fixture(response, fixture_file_name)
+ end
+end
+
+describe API::Issues, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+
+ def get_related_merge_requests(project_id, issue_iid, user = nil)
+ get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
+ end
+
+ def create_referencing_mr(user, project, issue)
+ attributes = {
+ author: user,
+ source_project: project,
+ target_project: project,
+ source_branch: "master",
+ target_branch: "test",
+ assignee: user,
+ description: "See #{issue.to_reference}"
+ }
+ create(:merge_request, attributes).tap do |merge_request|
+ create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
+ end
+ end
+
+ it 'issues/related_merge_requests.json' do
+ user = create(:user)
+ project = create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ issue_title = 'foo'
+ issue_description = 'closed'
+ milestone = create(:milestone, title: '1.0.0', project: project)
+ issue = create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+
+ project.add_reporter(user)
+ create_referencing_mr(user, project, issue)
+
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "Some description")
+ project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline))
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect(response).to be_success
end
end
diff --git a/spec/javascripts/fixtures/jobs.rb b/spec/javascripts/fixtures/jobs.rb
index 433bb690a1c..46ccd6f8c8a 100644
--- a/spec/javascripts/fixtures/jobs.rb
+++ b/spec/javascripts/fixtures/jobs.rb
@@ -32,7 +32,7 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
remove_repository(project)
end
- it 'builds/build-with-artifacts.html.raw' do |example|
+ it 'builds/build-with-artifacts.html' do
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -40,10 +40,9 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
- it 'jobs/delayed.json' do |example|
+ it 'jobs/delayed.json' do
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -51,6 +50,5 @@ describe Projects::JobsController, '(JavaScript fixtures)', type: :controller do
}, format: :json
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/labels.rb b/spec/javascripts/fixtures/labels.rb
index 9420194e675..4d1b7317274 100644
--- a/spec/javascripts/fixtures/labels.rb
+++ b/spec/javascripts/fixtures/labels.rb
@@ -30,13 +30,12 @@ describe 'Labels (JavaScript fixtures)' do
sign_in(admin)
end
- it 'labels/group_labels.json' do |example|
+ it 'labels/group_labels.json' do
get :index, params: {
group_id: group
}, format: 'json'
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
@@ -47,14 +46,13 @@ describe 'Labels (JavaScript fixtures)' do
sign_in(admin)
end
- it 'labels/project_labels.json' do |example|
+ it 'labels/project_labels.json' do
get :index, params: {
namespace_id: group,
project_id: project
}, format: 'json'
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
end
diff --git a/spec/javascripts/fixtures/line_highlighter.html.haml b/spec/javascripts/fixtures/line_highlighter.html.haml
deleted file mode 100644
index 2782c50e298..00000000000
--- a/spec/javascripts/fixtures/line_highlighter.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-.file-holder
- .file-content
- .line-numbers
- - 1.upto(25) do |i|
- %a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
- %i.fa.fa-link
- = i
- %pre.code.highlight
- %code
- - 1.upto(25) do |i|
- %span.line{id: "LC#{i}"}= "Line #{i}"
diff --git a/spec/javascripts/fixtures/linked_tabs.html.haml b/spec/javascripts/fixtures/linked_tabs.html.haml
deleted file mode 100644
index 632606e0536..00000000000
--- a/spec/javascripts/fixtures/linked_tabs.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%ul.nav.nav-tabs.new-session-tabs.linked-tabs
- %li.nav-item
- %a.nav-link{ href: 'foo/bar/1', data: { target: 'div#tab1', action: 'tab1', toggle: 'tab' } }
- Tab 1
- %li.nav-item
- %a.nav-link{ href: 'foo/bar/1/context', data: { target: 'div#tab2', action: 'tab2', toggle: 'tab' } }
- Tab 2
-
-.tab-content
- #tab1.tab-pane
- Tab 1 Content
- #tab2.tab-pane
- Tab 2 Content
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index eb37be87e1d..05860be2291 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -42,52 +42,52 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
remove_repository(project)
end
- it 'merge_requests/merge_request_of_current_user.html.raw' do |example|
+ it 'merge_requests/merge_request_of_current_user.html' do
merge_request.update(author: admin)
- render_merge_request(example.description, merge_request)
+ render_merge_request(merge_request)
end
- it 'merge_requests/merge_request_with_task_list.html.raw' do |example|
+ it 'merge_requests/merge_request_with_task_list.html' do
create(:ci_build, :pending, pipeline: pipeline)
- render_merge_request(example.description, merge_request)
+ render_merge_request(merge_request)
end
- it 'merge_requests/merged_merge_request.html.raw' do |example|
+ it 'merge_requests/merged_merge_request.html' do
expect_next_instance_of(MergeRequest) do |merge_request|
allow(merge_request).to receive(:source_branch_exists?).and_return(true)
allow(merge_request).to receive(:can_remove_source_branch?).and_return(true)
end
- render_merge_request(example.description, merged_merge_request)
+ render_merge_request(merged_merge_request)
end
- it 'merge_requests/diff_comment.html.raw' do |example|
+ it 'merge_requests/diff_comment.html' do
create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
create(:note_on_merge_request, author: admin, project: project, noteable: merge_request)
- render_merge_request(example.description, merge_request)
+ render_merge_request(merge_request)
end
- it 'merge_requests/merge_request_with_comment.html.raw' do |example|
+ it 'merge_requests/merge_request_with_comment.html' do
create(:note_on_merge_request, author: admin, project: project, noteable: merge_request, note: '- [ ] Task List Item')
- render_merge_request(example.description, merge_request)
+ render_merge_request(merge_request)
end
- it 'merge_requests/discussions.json' do |example|
+ it 'merge_requests/discussions.json' do
create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
- render_discussions_json(merge_request, example.description)
+ render_discussions_json(merge_request)
end
- it 'merge_requests/diff_discussion.json' do |example|
+ it 'merge_requests/diff_discussion.json' do
create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
- render_discussions_json(merge_request, example.description)
+ render_discussions_json(merge_request)
end
- it 'merge_requests/resolved_diff_discussion.json' do |example|
+ it 'merge_requests/resolved_diff_discussion.json' do
note = create(:discussion_note_on_merge_request, :resolved, project: project, author: admin, position: position, noteable: merge_request)
create(:system_note, project: project, author: admin, noteable: merge_request, discussion_id: note.discussion.id)
- render_discussions_json(merge_request, example.description)
+ render_discussions_json(merge_request)
end
context 'with image diff' do
@@ -106,25 +106,23 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
)
end
- it 'merge_requests/image_diff_discussion.json' do |example|
+ it 'merge_requests/image_diff_discussion.json' do
create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position)
- render_discussions_json(merge_request2, example.description)
+ render_discussions_json(merge_request2)
end
end
private
- def render_discussions_json(merge_request, fixture_file_name)
+ def render_discussions_json(merge_request)
get :discussions, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.to_param
}, format: :json
-
- store_frontend_fixture(response, fixture_file_name)
end
- def render_merge_request(fixture_file_name, merge_request)
+ def render_merge_request(merge_request)
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -132,6 +130,5 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
}, format: :html
expect(response).to be_success
- store_frontend_fixture(response, fixture_file_name)
end
end
diff --git a/spec/javascripts/fixtures/merge_requests_diffs.rb b/spec/javascripts/fixtures/merge_requests_diffs.rb
index 57462e74bb2..03b9b713fd8 100644
--- a/spec/javascripts/fixtures/merge_requests_diffs.rb
+++ b/spec/javascripts/fixtures/merge_requests_diffs.rb
@@ -34,29 +34,29 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type
remove_repository(project)
end
- it 'merge_request_diffs/with_commit.json' do |example|
+ it 'merge_request_diffs/with_commit.json' do
# Create a user that matches the selected commit author
# This is so that the "author" information will be populated
create(:user, email: selected_commit.author_email, name: selected_commit.author_name)
- render_merge_request(example.description, merge_request, commit_id: selected_commit.sha)
+ render_merge_request(merge_request, commit_id: selected_commit.sha)
end
- it 'merge_request_diffs/inline_changes_tab_with_comments.json' do |example|
+ it 'merge_request_diffs/inline_changes_tab_with_comments.json' do
create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
create(:note_on_merge_request, author: admin, project: project, noteable: merge_request)
- render_merge_request(example.description, merge_request)
+ render_merge_request(merge_request)
end
- it 'merge_request_diffs/parallel_changes_tab_with_comments.json' do |example|
+ it 'merge_request_diffs/parallel_changes_tab_with_comments.json' do
create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
create(:note_on_merge_request, author: admin, project: project, noteable: merge_request)
- render_merge_request(example.description, merge_request, view: 'parallel')
+ render_merge_request(merge_request, view: 'parallel')
end
private
- def render_merge_request(fixture_file_name, merge_request, view: 'inline', **extra_params)
+ def render_merge_request(merge_request, view: 'inline', **extra_params)
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -66,6 +66,5 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type
}, format: :json
expect(response).to be_success
- store_frontend_fixture(response, fixture_file_name)
end
end
diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml
deleted file mode 100644
index 8447dfdda32..00000000000
--- a/spec/javascripts/fixtures/merge_requests_show.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%a.btn-close
-
-.detail-page-description
- .description.js-task-list-container
- .wiki
- %ul.task-list
- %li.task-list-item
- %input.task-list-item-checkbox{type: 'checkbox'}
- Task List Item
- %textarea.js-task-list-field
- \- [ ] Task List Item
-
-%form.js-issuable-update{action: '/foo'}
diff --git a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml b/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
deleted file mode 100644
index 74584993739..00000000000
--- a/spec/javascripts/fixtures/mini_dropdown_graph.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%div.js-builds-dropdown-tests.dropdown.dropdown.js-mini-pipeline-graph
- %button.js-builds-dropdown-button{'data-stage-endpoint' => 'foobar', data: { toggle: 'dropdown'} }
- Dropdown
-
- %ul.dropdown-menu.mini-pipeline-graph-dropdown-menu.js-builds-dropdown-container
- %li.js-builds-dropdown-list.scrollable-menu
- %ul
-
- %li.js-builds-dropdown-loading.hidden
- %span.fa.fa-spinner
diff --git a/spec/javascripts/fixtures/notebook_viewer.html.haml b/spec/javascripts/fixtures/notebook_viewer.html.haml
deleted file mode 100644
index 17a7a9d8f31..00000000000
--- a/spec/javascripts/fixtures/notebook_viewer.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-.file-content#js-notebook-viewer{ data: { endpoint: '/test' } }
diff --git a/spec/javascripts/fixtures/oauth_remember_me.html.haml b/spec/javascripts/fixtures/oauth_remember_me.html.haml
deleted file mode 100644
index a5d7c4e816a..00000000000
--- a/spec/javascripts/fixtures/oauth_remember_me.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-#oauth-container
- %input#remember_me{ type: "checkbox" }
-
- %a.oauth-login.twitter{ href: "http://example.com/" }
- %a.oauth-login.github{ href: "http://example.com/" }
- %a.oauth-login.facebook{ href: "http://example.com/?redirect_fragment=L1" }
diff --git a/spec/javascripts/fixtures/pdf.rb b/spec/javascripts/fixtures/pdf.rb
deleted file mode 100644
index ef9976b9fd3..00000000000
--- a/spec/javascripts/fixtures/pdf.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'spec_helper'
-
-describe 'PDF file', '(JavaScript fixtures)', type: :controller do
- include JavaScriptFixturesHelpers
-
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project, :repository, namespace: namespace, path: 'pdf-project') }
-
- before(:all) do
- clean_frontend_fixtures('blob/pdf/')
- end
-
- it 'blob/pdf/test.pdf' do |example|
- blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf')
-
- store_frontend_fixture(blob.data.force_encoding("utf-8"), example.description)
- end
-end
diff --git a/spec/javascripts/fixtures/pdf_viewer.html.haml b/spec/javascripts/fixtures/pdf_viewer.html.haml
deleted file mode 100644
index 2e57beae54b..00000000000
--- a/spec/javascripts/fixtures/pdf_viewer.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-.file-content#js-pdf-viewer{ data: { endpoint: '/test' } }
diff --git a/spec/javascripts/fixtures/pipeline_graph.html.haml b/spec/javascripts/fixtures/pipeline_graph.html.haml
deleted file mode 100644
index c0b5ab4411e..00000000000
--- a/spec/javascripts/fixtures/pipeline_graph.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%div.pipeline-visualization.js-pipeline-graph
- %ul.stage-column-list
- %li.stage-column
- .stage-name
- %a{:href => "/"}
- Test
- .builds-container
- %ul
- %li.build
- .curve
- %a
- %svg
- .ci-status-text
- stop_review
diff --git a/spec/javascripts/fixtures/pipeline_schedules.rb b/spec/javascripts/fixtures/pipeline_schedules.rb
index 05d79ec8de9..aecd56e6198 100644
--- a/spec/javascripts/fixtures/pipeline_schedules.rb
+++ b/spec/javascripts/fixtures/pipeline_schedules.rb
@@ -21,7 +21,7 @@ describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :
sign_in(admin)
end
- it 'pipeline_schedules/edit.html.raw' do |example|
+ it 'pipeline_schedules/edit.html' do
get :edit, params: {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -29,10 +29,9 @@ describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
- it 'pipeline_schedules/edit_with_variables.html.raw' do |example|
+ it 'pipeline_schedules/edit_with_variables.html' do
get :edit, params: {
namespace_id: project.namespace.to_param,
project_id: project,
@@ -40,6 +39,5 @@ describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/pipelines.html.haml b/spec/javascripts/fixtures/pipelines.html.haml
deleted file mode 100644
index 0161c0550d1..00000000000
--- a/spec/javascripts/fixtures/pipelines.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-%div
- #pipelines-list-vue{ data: { endpoint: 'foo',
- "help-page-path" => 'foo',
- "help-auto-devops-path" => 'foo',
- "empty-state-svg-path" => 'foo',
- "error-state-svg-path" => 'foo',
- "new-pipeline-path" => 'foo',
- "can-create-pipeline" => 'true',
- "has-ci" => 'foo',
- "ci-lint-path" => 'foo',
- "reset-cache-path" => 'foo' } }
-
diff --git a/spec/javascripts/fixtures/pipelines.rb b/spec/javascripts/fixtures/pipelines.rb
index 42b552e81c0..de6fcfe10f4 100644
--- a/spec/javascripts/fixtures/pipelines.rb
+++ b/spec/javascripts/fixtures/pipelines.rb
@@ -23,13 +23,12 @@ describe Projects::PipelinesController, '(JavaScript fixtures)', type: :controll
sign_in(admin)
end
- it 'pipelines/pipelines.json' do |example|
+ it 'pipelines/pipelines.json' do
get :index, params: {
namespace_id: namespace,
project_id: project
}, format: :json
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/project_select_combo_button.html.haml b/spec/javascripts/fixtures/project_select_combo_button.html.haml
deleted file mode 100644
index 432cd5fcc74..00000000000
--- a/spec/javascripts/fixtures/project_select_combo_button.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-.project-item-select-holder
- %input.project-item-select{ data: { group_id: '12345' , relative_path: 'issues/new' } }
- %a.new-project-item-link{ data: { label: 'New issue', type: 'issues' }, href: ''}
- %i.fa.fa-spinner.spin
- %a.new-project-item-select-button
- %i.fa.fa-caret-down
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index 85f02923804..94c59207898 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -28,49 +28,45 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
end
describe ProjectsController, '(JavaScript fixtures)', type: :controller do
- it 'projects/dashboard.html.raw' do |example|
+ it 'projects/dashboard.html' do
get :show, params: {
namespace_id: project.namespace.to_param,
id: project
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
- it 'projects/overview.html.raw' do |example|
+ it 'projects/overview.html' do
get :show, params: {
namespace_id: project_with_repo.namespace.to_param,
id: project_with_repo
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
- it 'projects/edit.html.raw' do |example|
+ it 'projects/edit.html' do
get :edit, params: {
namespace_id: project.namespace.to_param,
id: project
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
describe Projects::Settings::CiCdController, '(JavaScript fixtures)', type: :controller do
- it 'projects/ci_cd_settings.html.raw' do |example|
+ it 'projects/ci_cd_settings.html' do
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
- it 'projects/ci_cd_settings_with_variables.html.raw' do |example|
+ it 'projects/ci_cd_settings_with_variables.html' do
create(:ci_variable, project: project_variable_populated)
create(:ci_variable, project: project_variable_populated)
@@ -80,7 +76,6 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
end
diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb
index 746fbfd66dd..f3171fdd97b 100644
--- a/spec/javascripts/fixtures/prometheus_service.rb
+++ b/spec/javascripts/fixtures/prometheus_service.rb
@@ -22,7 +22,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
remove_repository(project)
end
- it 'services/prometheus/prometheus_service.html.raw' do |example|
+ it 'services/prometheus/prometheus_service.html' do
get :edit, params: {
namespace_id: namespace,
project_id: project,
@@ -30,6 +30,5 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/raw.rb b/spec/javascripts/fixtures/raw.rb
index 82770beb39b..801c80a0112 100644
--- a/spec/javascripts/fixtures/raw.rb
+++ b/spec/javascripts/fixtures/raw.rb
@@ -1,34 +1,39 @@
require 'spec_helper'
-describe 'Raw files', '(JavaScript fixtures)', type: :controller do
+describe 'Raw files', '(JavaScript fixtures)' do
include JavaScriptFixturesHelpers
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
let(:project) { create(:project, :repository, namespace: namespace, path: 'raw-project') }
+ let(:response) { @blob.data.force_encoding('UTF-8') }
before(:all) do
+ clean_frontend_fixtures('blob/balsamiq/')
clean_frontend_fixtures('blob/notebook/')
+ clean_frontend_fixtures('blob/pdf/')
end
after do
remove_repository(project)
end
- it 'blob/notebook/basic.json' do |example|
- blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb')
-
- store_frontend_fixture(blob.data, example.description)
+ it 'blob/balsamiq/test.bmpr' do
+ @blob = project.repository.blob_at('b89b56d79', 'files/images/balsamiq.bmpr')
end
- it 'blob/notebook/worksheets.json' do |example|
- blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb')
+ it 'blob/notebook/basic.json' do
+ @blob = project.repository.blob_at('6d85bb69', 'files/ipython/basic.ipynb')
+ end
- store_frontend_fixture(blob.data, example.description)
+ it 'blob/notebook/worksheets.json' do
+ @blob = project.repository.blob_at('6d85bb69', 'files/ipython/worksheets.ipynb')
end
- it 'blob/notebook/math.json' do |example|
- blob = project.repository.blob_at('93ee732', 'files/ipython/math.ipynb')
+ it 'blob/notebook/math.json' do
+ @blob = project.repository.blob_at('93ee732', 'files/ipython/math.ipynb')
+ end
- store_frontend_fixture(blob.data, example.description)
+ it 'blob/pdf/test.pdf' do
+ @blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf')
end
end
diff --git a/spec/javascripts/fixtures/search.rb b/spec/javascripts/fixtures/search.rb
index 703cd3d49fa..22fc546d761 100644
--- a/spec/javascripts/fixtures/search.rb
+++ b/spec/javascripts/fixtures/search.rb
@@ -9,10 +9,9 @@ describe SearchController, '(JavaScript fixtures)', type: :controller do
clean_frontend_fixtures('search/')
end
- it 'search/show.html.raw' do |example|
+ it 'search/show.html' do
get :show
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/search_autocomplete.html.haml b/spec/javascripts/fixtures/search_autocomplete.html.haml
deleted file mode 100644
index 4aa54da9411..00000000000
--- a/spec/javascripts/fixtures/search_autocomplete.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-.search.search-form
- %form.form-inline
- .search-input-container
- .search-input-wrap
- .dropdown
- %input#search.search-input.dropdown-menu-toggle
- .dropdown-menu.dropdown-select
- .dropdown-content
- %input{ type: "hidden", class: "js-search-project-options" }
diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb
index 6ccd74a07ff..2237702ccca 100644
--- a/spec/javascripts/fixtures/services.rb
+++ b/spec/javascripts/fixtures/services.rb
@@ -22,7 +22,7 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
remove_repository(project)
end
- it 'services/edit_service.html.raw' do |example|
+ it 'services/edit_service.html' do
get :edit, params: {
namespace_id: namespace,
project_id: project,
@@ -30,6 +30,5 @@ describe Projects::ServicesController, '(JavaScript fixtures)', type: :controlle
}
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/sessions.rb b/spec/javascripts/fixtures/sessions.rb
index e90a58e8c54..92b74c01c89 100644
--- a/spec/javascripts/fixtures/sessions.rb
+++ b/spec/javascripts/fixtures/sessions.rb
@@ -16,11 +16,10 @@ describe 'Sessions (JavaScript fixtures)' do
set_devise_mapping(context: @request)
end
- it 'sessions/new.html.raw' do |example|
+ it 'sessions/new.html' do
get :new
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
end
diff --git a/spec/javascripts/fixtures/signin_tabs.html.haml b/spec/javascripts/fixtures/signin_tabs.html.haml
deleted file mode 100644
index 2e00fe7865e..00000000000
--- a/spec/javascripts/fixtures/signin_tabs.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-%ul.nav-links.new-session-tabs
- %li.active
- %a{ href: '#ldap' } LDAP
- %li
- %a{ href: '#login-pane'} Standard
diff --git a/spec/javascripts/fixtures/sketch_viewer.html.haml b/spec/javascripts/fixtures/sketch_viewer.html.haml
deleted file mode 100644
index f01bd00925a..00000000000
--- a/spec/javascripts/fixtures/sketch_viewer.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-.file-content#js-sketch-viewer{ data: { endpoint: '/test_sketch_file.sketch' } }
- .js-loading-icon
diff --git a/spec/javascripts/fixtures/snippet.rb b/spec/javascripts/fixtures/snippet.rb
index bcd6546f3df..ace84b14eb7 100644
--- a/spec/javascripts/fixtures/snippet.rb
+++ b/spec/javascripts/fixtures/snippet.rb
@@ -23,12 +23,11 @@ describe SnippetsController, '(JavaScript fixtures)', type: :controller do
remove_repository(project)
end
- it 'snippets/show.html.raw' do |example|
+ it 'snippets/show.html' do
create(:discussion_note_on_snippet, noteable: snippet, project: project, author: admin, note: '- [ ] Task List Item')
get(:show, params: { id: snippet.to_param })
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
diff --git a/spec/javascripts/fixtures/static/README.md b/spec/javascripts/fixtures/static/README.md
new file mode 100644
index 00000000000..b5c2f8233bf
--- /dev/null
+++ b/spec/javascripts/fixtures/static/README.md
@@ -0,0 +1,3 @@
+# Please do not add new files here!
+
+Instead use a Ruby file in the fixtures root directory (`spec/javascripts/fixtures/`).
diff --git a/spec/javascripts/fixtures/static/ajax_loading_spinner.html b/spec/javascripts/fixtures/static/ajax_loading_spinner.html
new file mode 100644
index 00000000000..0e1ebb32b1c
--- /dev/null
+++ b/spec/javascripts/fixtures/static/ajax_loading_spinner.html
@@ -0,0 +1,3 @@
+<a class="js-ajax-loading-spinner" data-remote href="http://goesnowhere.nothing/whereami">
+<i class="fa fa-trash-o"></i>
+</a>
diff --git a/spec/javascripts/fixtures/static/balsamiq_viewer.html b/spec/javascripts/fixtures/static/balsamiq_viewer.html
new file mode 100644
index 00000000000..cdd723d1a84
--- /dev/null
+++ b/spec/javascripts/fixtures/static/balsamiq_viewer.html
@@ -0,0 +1 @@
+<div class="file-content balsamiq-viewer" data-endpoint="/test" id="js-balsamiq-viewer"></div>
diff --git a/spec/javascripts/fixtures/static/create_item_dropdown.html b/spec/javascripts/fixtures/static/create_item_dropdown.html
new file mode 100644
index 00000000000..d2d38370092
--- /dev/null
+++ b/spec/javascripts/fixtures/static/create_item_dropdown.html
@@ -0,0 +1,11 @@
+<div class="js-create-item-dropdown-fixture-root">
+<input name="variable[environment]" type="hidden">
+<div class="dropdown "><button class="dropdown-menu-toggle js-dropdown-menu-toggle" type="button" data-toggle="dropdown"><span class="dropdown-toggle-text ">some label</span><i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i></button><div class="dropdown-menu dropdown-select dropdown-menu-selectable"><div class="dropdown-input"><input type="search" id="" class="dropdown-input-field" autocomplete="off" /><i aria-hidden="true" data-hidden="true" class="fa fa-search dropdown-input-search"></i><i aria-hidden="true" data-hidden="true" role="button" class="fa fa-times dropdown-input-clear js-dropdown-input-clear"></i></div><div class="dropdown-content js-dropdown-content"></div><div class="dropdown-footer"><ul class="dropdown-footer-list">
+<li>
+<button class="dropdown-create-new-item-button js-dropdown-create-new-item">
+Create wildcard
+<code></code>
+</button>
+</li>
+</ul>
+</div><div class="dropdown-loading"><i aria-hidden="true" data-hidden="true" class="fa fa-spinner fa-spin"></i></div></div></div></div>
diff --git a/spec/javascripts/fixtures/static/environments/table.html b/spec/javascripts/fixtures/static/environments/table.html
new file mode 100644
index 00000000000..417af564ff1
--- /dev/null
+++ b/spec/javascripts/fixtures/static/environments/table.html
@@ -0,0 +1,15 @@
+<table>
+<thead>
+<tr>
+<th>Environment</th>
+<th>Last deployment</th>
+<th>Job</th>
+<th>Commit</th>
+<th></th>
+<th></th>
+</tr>
+</thead>
+<tbody>
+<tr id="environment-row"></tr>
+</tbody>
+</table>
diff --git a/spec/javascripts/fixtures/static/event_filter.html b/spec/javascripts/fixtures/static/event_filter.html
new file mode 100644
index 00000000000..8e9b6fb1b5c
--- /dev/null
+++ b/spec/javascripts/fixtures/static/event_filter.html
@@ -0,0 +1,44 @@
+<ul class="nav-links event-filter scrolling-tabs nav nav-tabs">
+<li class="active">
+<a class="event-filter-link" href="/dashboard/activity" id="all_event_filter" title="Filter by all">
+<span>
+All
+</span>
+</a>
+</li>
+<li>
+<a class="event-filter-link" href="/dashboard/activity" id="push_event_filter" title="Filter by push events">
+<span>
+Push events
+</span>
+</a>
+</li>
+<li>
+<a class="event-filter-link" href="/dashboard/activity" id="merged_event_filter" title="Filter by merge events">
+<span>
+Merge events
+</span>
+</a>
+</li>
+<li>
+<a class="event-filter-link" href="/dashboard/activity" id="issue_event_filter" title="Filter by issue events">
+<span>
+Issue events
+</span>
+</a>
+</li>
+<li>
+<a class="event-filter-link" href="/dashboard/activity" id="comments_event_filter" title="Filter by comments">
+<span>
+Comments
+</span>
+</a>
+</li>
+<li>
+<a class="event-filter-link" href="/dashboard/activity" id="team_event_filter" title="Filter by team">
+<span>
+Team
+</span>
+</a>
+</li>
+</ul>
diff --git a/spec/javascripts/fixtures/static/gl_dropdown.html b/spec/javascripts/fixtures/static/gl_dropdown.html
new file mode 100644
index 00000000000..08f6738414e
--- /dev/null
+++ b/spec/javascripts/fixtures/static/gl_dropdown.html
@@ -0,0 +1,26 @@
+<div>
+<div class="dropdown inline">
+<button class="dropdown-menu-toggle" data-toggle="dropdown" id="js-project-dropdown" type="button">
+<div class="dropdown-toggle-text">
+Projects
+</div>
+<i class="fa fa-chevron-down dropdown-toggle-caret js-projects-dropdown-toggle"></i>
+</button>
+<div class="dropdown-menu dropdown-select dropdown-menu-selectable">
+<div class="dropdown-title">
+<span>Go to project</span>
+<button aria="{:label=&gt;&quot;Close&quot;}" class="dropdown-title-button dropdown-menu-close">
+<i class="fa fa-times dropdown-menu-close-icon"></i>
+</button>
+</div>
+<div class="dropdown-input">
+<input class="dropdown-input-field" placeholder="Filter results" type="search">
+<i class="fa fa-search dropdown-input-search"></i>
+</div>
+<div class="dropdown-content"></div>
+<div class="dropdown-loading">
+<i class="fa fa-spinner fa-spin"></i>
+</div>
+</div>
+</div>
+</div>
diff --git a/spec/javascripts/fixtures/static/gl_field_errors.html b/spec/javascripts/fixtures/static/gl_field_errors.html
new file mode 100644
index 00000000000..f8470e02b7c
--- /dev/null
+++ b/spec/javascripts/fixtures/static/gl_field_errors.html
@@ -0,0 +1,22 @@
+<form action="submit" class="gl-show-field-errors" method="post">
+<div class="form-group">
+<input class="required-text" required type="text">Text</input>
+</div>
+<div class="form-group">
+<input class="email" required title="Please provide a valid email address." type="email">Email</input>
+</div>
+<div class="form-group">
+<input class="password" required type="password">Password</input>
+</div>
+<div class="form-group">
+<input class="alphanumeric" pattern="[a-zA-Z0-9]" required type="text">Alphanumeric</input>
+</div>
+<div class="form-group">
+<input class="hidden" type="hidden">
+</div>
+<div class="form-group">
+<input class="custom gl-field-error-ignore" type="text">Custom, do not validate</input>
+</div>
+<div class="form-group"></div>
+<input class="submit" type="submit">Submit</input>
+</form>
diff --git a/spec/javascripts/fixtures/images/green_box.png b/spec/javascripts/fixtures/static/images/green_box.png
index cd1ff9f9ade..cd1ff9f9ade 100644
--- a/spec/javascripts/fixtures/images/green_box.png
+++ b/spec/javascripts/fixtures/static/images/green_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/one_white_pixel.png b/spec/javascripts/fixtures/static/images/one_white_pixel.png
index 073fcf40a18..073fcf40a18 100644
--- a/spec/javascripts/fixtures/one_white_pixel.png
+++ b/spec/javascripts/fixtures/static/images/one_white_pixel.png
Binary files differ
diff --git a/spec/javascripts/fixtures/images/red_box.png b/spec/javascripts/fixtures/static/images/red_box.png
index 73b2927da0f..73b2927da0f 100644
--- a/spec/javascripts/fixtures/images/red_box.png
+++ b/spec/javascripts/fixtures/static/images/red_box.png
Binary files differ
diff --git a/spec/javascripts/fixtures/static/issuable_filter.html b/spec/javascripts/fixtures/static/issuable_filter.html
new file mode 100644
index 00000000000..06b70fb43f1
--- /dev/null
+++ b/spec/javascripts/fixtures/static/issuable_filter.html
@@ -0,0 +1,9 @@
+<form action="/user/project/issues?scope=all&amp;state=closed" class="js-filter-form">
+<input id="utf8" name="utf8" value="✓">
+<input id="check-all-issues" name="check-all-issues">
+<input id="search" name="search">
+<input id="author_id" name="author_id">
+<input id="assignee_id" name="assignee_id">
+<input id="milestone_title" name="milestone_title">
+<input id="label_name" name="label_name">
+</form>
diff --git a/spec/javascripts/fixtures/static/issue_sidebar_label.html b/spec/javascripts/fixtures/static/issue_sidebar_label.html
new file mode 100644
index 00000000000..ec8fb30f219
--- /dev/null
+++ b/spec/javascripts/fixtures/static/issue_sidebar_label.html
@@ -0,0 +1,26 @@
+<div class="block labels">
+<div class="sidebar-collapsed-icon js-sidebar-labels-tooltip"></div>
+<div class="title hide-collapsed">
+<a class="edit-link float-right" href="#">
+Edit
+</a>
+</div>
+<div class="selectbox hide-collapsed" style="display: none;">
+<div class="dropdown">
+<button class="dropdown-menu-toggle js-label-select js-multiselect" data-ability-name="issue" data-field-name="issue[label_names][]" data-issue-update="/root/test/issues/2.json" data-labels="/root/test/labels.json" data-project-id="12" data-show-any="true" data-show-no="true" data-toggle="dropdown" type="button">
+<span class="dropdown-toggle-text">
+Label
+</span>
+<i class="fa fa-chevron-down"></i>
+</button>
+<div class="dropdown-menu dropdown-select dropdown-menu-paging dropdown-menu-labels dropdown-menu-selectable">
+<div class="dropdown-page-one">
+<div class="dropdown-content"></div>
+<div class="dropdown-loading">
+<i class="fa fa-spinner fa-spin"></i>
+</div>
+</div>
+</div>
+</div>
+</div>
+</div>
diff --git a/spec/javascripts/fixtures/static/line_highlighter.html b/spec/javascripts/fixtures/static/line_highlighter.html
new file mode 100644
index 00000000000..897a25d6760
--- /dev/null
+++ b/spec/javascripts/fixtures/static/line_highlighter.html
@@ -0,0 +1,107 @@
+<div class="file-holder">
+<div class="file-content">
+<div class="line-numbers">
+<a data-line-number="1" href="#L1" id="L1">
+<i class="fa fa-link"></i>
+1
+</a>
+<a data-line-number="2" href="#L2" id="L2">
+<i class="fa fa-link"></i>
+2
+</a>
+<a data-line-number="3" href="#L3" id="L3">
+<i class="fa fa-link"></i>
+3
+</a>
+<a data-line-number="4" href="#L4" id="L4">
+<i class="fa fa-link"></i>
+4
+</a>
+<a data-line-number="5" href="#L5" id="L5">
+<i class="fa fa-link"></i>
+5
+</a>
+<a data-line-number="6" href="#L6" id="L6">
+<i class="fa fa-link"></i>
+6
+</a>
+<a data-line-number="7" href="#L7" id="L7">
+<i class="fa fa-link"></i>
+7
+</a>
+<a data-line-number="8" href="#L8" id="L8">
+<i class="fa fa-link"></i>
+8
+</a>
+<a data-line-number="9" href="#L9" id="L9">
+<i class="fa fa-link"></i>
+9
+</a>
+<a data-line-number="10" href="#L10" id="L10">
+<i class="fa fa-link"></i>
+10
+</a>
+<a data-line-number="11" href="#L11" id="L11">
+<i class="fa fa-link"></i>
+11
+</a>
+<a data-line-number="12" href="#L12" id="L12">
+<i class="fa fa-link"></i>
+12
+</a>
+<a data-line-number="13" href="#L13" id="L13">
+<i class="fa fa-link"></i>
+13
+</a>
+<a data-line-number="14" href="#L14" id="L14">
+<i class="fa fa-link"></i>
+14
+</a>
+<a data-line-number="15" href="#L15" id="L15">
+<i class="fa fa-link"></i>
+15
+</a>
+<a data-line-number="16" href="#L16" id="L16">
+<i class="fa fa-link"></i>
+16
+</a>
+<a data-line-number="17" href="#L17" id="L17">
+<i class="fa fa-link"></i>
+17
+</a>
+<a data-line-number="18" href="#L18" id="L18">
+<i class="fa fa-link"></i>
+18
+</a>
+<a data-line-number="19" href="#L19" id="L19">
+<i class="fa fa-link"></i>
+19
+</a>
+<a data-line-number="20" href="#L20" id="L20">
+<i class="fa fa-link"></i>
+20
+</a>
+<a data-line-number="21" href="#L21" id="L21">
+<i class="fa fa-link"></i>
+21
+</a>
+<a data-line-number="22" href="#L22" id="L22">
+<i class="fa fa-link"></i>
+22
+</a>
+<a data-line-number="23" href="#L23" id="L23">
+<i class="fa fa-link"></i>
+23
+</a>
+<a data-line-number="24" href="#L24" id="L24">
+<i class="fa fa-link"></i>
+24
+</a>
+<a data-line-number="25" href="#L25" id="L25">
+<i class="fa fa-link"></i>
+25
+</a>
+</div>
+<pre class="code highlight"><code><span class="line" id="LC1">Line 1</span><span class="line" id="LC2">Line 2</span><span class="line" id="LC3">Line 3</span><span class="line" id="LC4">Line 4</span><span class="line" id="LC5">Line 5</span><span class="line" id="LC6">Line 6</span><span class="line" id="LC7">Line 7</span><span class="line" id="LC8">Line 8</span><span class="line" id="LC9">Line 9</span><span class="line" id="LC10">Line 10</span><span class="line" id="LC11">Line 11</span><span class="line" id="LC12">Line 12</span><span class="line" id="LC13">Line 13</span><span class="line" id="LC14">Line 14</span><span class="line" id="LC15">Line 15</span><span class="line" id="LC16">Line 16</span><span class="line" id="LC17">Line 17</span><span class="line" id="LC18">Line 18</span><span class="line" id="LC19">Line 19</span><span class="line" id="LC20">Line 20</span><span class="line" id="LC21">Line 21</span><span class="line" id="LC22">Line 22</span><span class="line" id="LC23">Line 23</span><span class="line" id="LC24">Line 24</span><span class="line" id="LC25">Line 25</span></code></pre>
+</div>
+</div>
diff --git a/spec/javascripts/fixtures/static/linked_tabs.html b/spec/javascripts/fixtures/static/linked_tabs.html
new file mode 100644
index 00000000000..c25463bf1db
--- /dev/null
+++ b/spec/javascripts/fixtures/static/linked_tabs.html
@@ -0,0 +1,20 @@
+<ul class="nav nav-tabs new-session-tabs linked-tabs">
+<li class="nav-item">
+<a class="nav-link" data-action="tab1" data-target="div#tab1" data-toggle="tab" href="foo/bar/1">
+Tab 1
+</a>
+</li>
+<li class="nav-item">
+<a class="nav-link" data-action="tab2" data-target="div#tab2" data-toggle="tab" href="foo/bar/1/context">
+Tab 2
+</a>
+</li>
+</ul>
+<div class="tab-content">
+<div class="tab-pane" id="tab1">
+Tab 1 Content
+</div>
+<div class="tab-pane" id="tab2">
+Tab 2 Content
+</div>
+</div>
diff --git a/spec/javascripts/fixtures/static/merge_requests_show.html b/spec/javascripts/fixtures/static/merge_requests_show.html
new file mode 100644
index 00000000000..87e36c9f315
--- /dev/null
+++ b/spec/javascripts/fixtures/static/merge_requests_show.html
@@ -0,0 +1,15 @@
+<a class="btn-close"></a>
+<div class="detail-page-description">
+<div class="description js-task-list-container">
+<div class="md">
+<ul class="task-list">
+<li class="task-list-item">
+<input class="task-list-item-checkbox" type="checkbox">
+Task List Item
+</li>
+</ul>
+<textarea class="js-task-list-field">- [ ] Task List Item</textarea>
+</div>
+</div>
+</div>
+<form action="/foo" class="js-issuable-update"></form>
diff --git a/spec/javascripts/fixtures/static/mini_dropdown_graph.html b/spec/javascripts/fixtures/static/mini_dropdown_graph.html
new file mode 100644
index 00000000000..cd0b8dec3fc
--- /dev/null
+++ b/spec/javascripts/fixtures/static/mini_dropdown_graph.html
@@ -0,0 +1,13 @@
+<div class="js-builds-dropdown-tests dropdown dropdown js-mini-pipeline-graph">
+<button class="js-builds-dropdown-button" data-toggle="dropdown" data-stage-endpoint="foobar">
+Dropdown
+</button>
+<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+<li class="js-builds-dropdown-list scrollable-menu">
+<ul></ul>
+</li>
+<li class="js-builds-dropdown-loading hidden">
+<span class="fa fa-spinner"></span>
+</li>
+</ul>
+</div>
diff --git a/spec/javascripts/fixtures/static/notebook_viewer.html b/spec/javascripts/fixtures/static/notebook_viewer.html
new file mode 100644
index 00000000000..4bbb7bf1094
--- /dev/null
+++ b/spec/javascripts/fixtures/static/notebook_viewer.html
@@ -0,0 +1 @@
+<div class="file-content" data-endpoint="/test" id="js-notebook-viewer"></div>
diff --git a/spec/javascripts/fixtures/static/oauth_remember_me.html b/spec/javascripts/fixtures/static/oauth_remember_me.html
new file mode 100644
index 00000000000..9ba1ffc72fe
--- /dev/null
+++ b/spec/javascripts/fixtures/static/oauth_remember_me.html
@@ -0,0 +1,6 @@
+<div id="oauth-container">
+<input id="remember_me" type="checkbox">
+<a class="oauth-login twitter" href="http://example.com/"></a>
+<a class="oauth-login github" href="http://example.com/"></a>
+<a class="oauth-login facebook" href="http://example.com/?redirect_fragment=L1"></a>
+</div>
diff --git a/spec/javascripts/fixtures/static/pdf_viewer.html b/spec/javascripts/fixtures/static/pdf_viewer.html
new file mode 100644
index 00000000000..350d35a262f
--- /dev/null
+++ b/spec/javascripts/fixtures/static/pdf_viewer.html
@@ -0,0 +1 @@
+<div class="file-content" data-endpoint="/test" id="js-pdf-viewer"></div>
diff --git a/spec/javascripts/fixtures/static/pipeline_graph.html b/spec/javascripts/fixtures/static/pipeline_graph.html
new file mode 100644
index 00000000000..422372bb7d5
--- /dev/null
+++ b/spec/javascripts/fixtures/static/pipeline_graph.html
@@ -0,0 +1,24 @@
+<div class="pipeline-visualization js-pipeline-graph">
+<ul class="stage-column-list">
+<li class="stage-column">
+<div class="stage-name">
+<a href="/">
+Test
+<div class="builds-container">
+<ul>
+<li class="build">
+<div class="curve"></div>
+<a>
+<svg></svg>
+<div class="ci-status-text">
+stop_review
+</div>
+</a>
+</li>
+</ul>
+</div>
+</a>
+</div>
+</li>
+</ul>
+</div>
diff --git a/spec/javascripts/fixtures/static/pipelines.html b/spec/javascripts/fixtures/static/pipelines.html
new file mode 100644
index 00000000000..42333f94f2f
--- /dev/null
+++ b/spec/javascripts/fixtures/static/pipelines.html
@@ -0,0 +1,3 @@
+<div>
+<div data-can-create-pipeline="true" data-ci-lint-path="foo" data-empty-state-svg-path="foo" data-endpoint="foo" data-error-state-svg-path="foo" data-has-ci="foo" data-help-auto-devops-path="foo" data-help-page-path="foo" data-new-pipeline-path="foo" data-reset-cache-path="foo" id="pipelines-list-vue"></div>
+</div>
diff --git a/spec/javascripts/fixtures/static/project_select_combo_button.html b/spec/javascripts/fixtures/static/project_select_combo_button.html
new file mode 100644
index 00000000000..50c826051c0
--- /dev/null
+++ b/spec/javascripts/fixtures/static/project_select_combo_button.html
@@ -0,0 +1,9 @@
+<div class="project-item-select-holder">
+<input class="project-item-select" data-group-id="12345" data-relative-path="issues/new">
+<a class="new-project-item-link" data-label="New issue" data-type="issues" href="">
+<i class="fa fa-spinner spin"></i>
+</a>
+<a class="new-project-item-select-button">
+<i class="fa fa-caret-down"></i>
+</a>
+</div>
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/static/projects.json
index 68a150f602a..68a150f602a 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/static/projects.json
diff --git a/spec/javascripts/fixtures/static/search_autocomplete.html b/spec/javascripts/fixtures/static/search_autocomplete.html
new file mode 100644
index 00000000000..29db9020424
--- /dev/null
+++ b/spec/javascripts/fixtures/static/search_autocomplete.html
@@ -0,0 +1,15 @@
+<div class="search search-form">
+<form class="form-inline">
+<div class="search-input-container">
+<div class="search-input-wrap">
+<div class="dropdown">
+<input class="search-input dropdown-menu-toggle" id="search">
+<div class="dropdown-menu dropdown-select">
+<div class="dropdown-content"></div>
+</div>
+</div>
+</div>
+</div>
+<input class="js-search-project-options" type="hidden">
+</form>
+</div>
diff --git a/spec/javascripts/fixtures/static/signin_tabs.html b/spec/javascripts/fixtures/static/signin_tabs.html
new file mode 100644
index 00000000000..7e66ab9394b
--- /dev/null
+++ b/spec/javascripts/fixtures/static/signin_tabs.html
@@ -0,0 +1,8 @@
+<ul class="nav-links new-session-tabs">
+<li class="active">
+<a href="#ldap">LDAP</a>
+</li>
+<li>
+<a href="#login-pane">Standard</a>
+</li>
+</ul>
diff --git a/spec/javascripts/fixtures/static/sketch_viewer.html b/spec/javascripts/fixtures/static/sketch_viewer.html
new file mode 100644
index 00000000000..e25e554e568
--- /dev/null
+++ b/spec/javascripts/fixtures/static/sketch_viewer.html
@@ -0,0 +1,3 @@
+<div class="file-content" data-endpoint="/test_sketch_file.sketch" id="js-sketch-viewer">
+<div class="js-loading-icon"></div>
+</div>
diff --git a/spec/javascripts/fixtures/static_fixtures.rb b/spec/javascripts/fixtures/static_fixtures.rb
deleted file mode 100644
index 4569f16f0ca..00000000000
--- a/spec/javascripts/fixtures/static_fixtures.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-require 'spec_helper'
-
-describe ApplicationController, '(Static JavaScript fixtures)', type: :controller do
- include JavaScriptFixturesHelpers
-
- before(:all) do
- clean_frontend_fixtures('static/')
- end
-
- fixtures_path = File.expand_path(JavaScriptFixturesHelpers::FIXTURE_PATH, Rails.root)
- haml_fixtures = Dir.glob(File.expand_path('**/*.haml', fixtures_path)).map do |file_path|
- file_path.sub(/\A#{fixtures_path}#{File::SEPARATOR}/, '')
- end
-
- haml_fixtures.each do |template_file_name|
- it "static/#{template_file_name.sub(/\.haml\z/, '.raw')}" do |example|
- fixture_file_name = example.description
- rendered = render_template(template_file_name)
- store_frontend_fixture(rendered, fixture_file_name)
- end
- end
-
- private
-
- def render_template(template_file_name)
- fixture_path = JavaScriptFixturesHelpers::FIXTURE_PATH
- controller = ApplicationController.new
- controller.prepend_view_path(fixture_path)
- controller.render_to_string(template: template_file_name, layout: false)
- end
-end
diff --git a/spec/javascripts/fixtures/todos.rb b/spec/javascripts/fixtures/todos.rb
index b5f6620873b..d0c8a6eca01 100644
--- a/spec/javascripts/fixtures/todos.rb
+++ b/spec/javascripts/fixtures/todos.rb
@@ -26,11 +26,10 @@ describe 'Todos (JavaScript fixtures)' do
sign_in(admin)
end
- it 'todos/todos.html.raw' do |example|
+ it 'todos/todos.html' do
get :index
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
@@ -41,7 +40,7 @@ describe 'Todos (JavaScript fixtures)' do
sign_in(admin)
end
- it 'todos/todos.json' do |example|
+ it 'todos/todos.json' do
post :create, params: {
namespace_id: namespace,
project_id: project,
@@ -50,7 +49,6 @@ describe 'Todos (JavaScript fixtures)' do
}, format: 'json'
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
end
diff --git a/spec/javascripts/fixtures/u2f.rb b/spec/javascripts/fixtures/u2f.rb
index 5cdbadef639..f52832b6efb 100644
--- a/spec/javascripts/fixtures/u2f.rb
+++ b/spec/javascripts/fixtures/u2f.rb
@@ -18,13 +18,12 @@ context 'U2F' do
set_devise_mapping(context: @request)
end
- it 'u2f/authenticate.html.raw' do |example|
+ it 'u2f/authenticate.html' do
allow(controller).to receive(:find_user).and_return(user)
post :create, params: { user: { login: user.username, password: user.password } }
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
@@ -36,11 +35,10 @@ context 'U2F' do
allow_any_instance_of(Profiles::TwoFactorAuthsController).to receive(:build_qr_code).and_return('qrcode:blackandwhitesquares')
end
- it 'u2f/register.html.raw' do |example|
+ it 'u2f/register.html' do
get :show
expect(response).to be_success
- store_frontend_fixture(response, example.description)
end
end
end
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index 7ef44f29c5b..4772f754937 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -14,6 +14,7 @@ import {
setSidebar,
subItemsMouseLeave,
} from '~/fly_out_nav';
+import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
import bp from '~/breakpoints';
describe('Fly out sidebar navigation', () => {
@@ -219,7 +220,7 @@ describe('Fly out sidebar navigation', () => {
it('shows collapsed only sub-items if icon only sidebar', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-collapsed-desktop');
+ sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
subItems.classList.add('is-fly-out-only');
setSidebar(sidebar);
@@ -296,7 +297,7 @@ describe('Fly out sidebar navigation', () => {
it('returns true when active & collapsed sidebar', () => {
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-collapsed-desktop');
+ sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
el.classList.add('active');
setSidebar(sidebar);
diff --git a/spec/javascripts/frequent_items/components/app_spec.js b/spec/javascripts/frequent_items/components/app_spec.js
index b1cc4d8dc8d..6814f656f5d 100644
--- a/spec/javascripts/frequent_items/components/app_spec.js
+++ b/spec/javascripts/frequent_items/components/app_spec.js
@@ -194,7 +194,7 @@ describe('Frequent Items App Component', () => {
expect(loadingEl).toBeDefined();
expect(loadingEl.classList.contains('prepend-top-20')).toBe(true);
- expect(loadingEl.querySelector('i').getAttribute('aria-label')).toBe('Loading projects');
+ expect(loadingEl.querySelector('span').getAttribute('aria-label')).toBe('Loading projects');
done();
});
});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
index 7deed985219..dce8e3be148 100644
--- a/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/javascripts/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,25 +1,31 @@
import Vue from 'vue';
import frequentItemsListItemComponent from '~/frequent_items/components/frequent_items_list_item.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
+import { trimText } from 'spec/helpers/text_helper';
import { mockProject } from '../mock_data'; // can also use 'mockGroup', but not useful to test here
const createComponent = () => {
const Component = Vue.extend(frequentItemsListItemComponent);
- return mountComponent(Component, {
- itemId: mockProject.id,
- itemName: mockProject.name,
- namespace: mockProject.namespace,
- webUrl: mockProject.webUrl,
- avatarUrl: mockProject.avatarUrl,
+ return shallowMount(Component, {
+ propsData: {
+ itemId: mockProject.id,
+ itemName: mockProject.name,
+ namespace: mockProject.namespace,
+ webUrl: mockProject.webUrl,
+ avatarUrl: mockProject.avatarUrl,
+ },
});
};
describe('FrequentItemsListItemComponent', () => {
+ let wrapper;
let vm;
beforeEach(() => {
- vm = createComponent();
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
});
afterEach(() => {
@@ -29,11 +35,11 @@ describe('FrequentItemsListItemComponent', () => {
describe('computed', () => {
describe('hasAvatar', () => {
it('should return `true` or `false` if whether avatar is present or not', () => {
- vm.avatarUrl = 'path/to/avatar.png';
+ wrapper.setProps({ avatarUrl: 'path/to/avatar.png' });
expect(vm.hasAvatar).toBe(true);
- vm.avatarUrl = null;
+ wrapper.setProps({ avatarUrl: null });
expect(vm.hasAvatar).toBe(false);
});
@@ -41,41 +47,49 @@ describe('FrequentItemsListItemComponent', () => {
describe('highlightedItemName', () => {
it('should enclose part of project name in <b> & </b> which matches with `matcher` prop', () => {
- vm.matcher = 'lab';
+ wrapper.setProps({ matcher: 'lab' });
- expect(vm.highlightedItemName).toContain('<b>Lab</b>');
+ expect(wrapper.find('.js-frequent-items-item-title').html()).toContain(
+ '<b>L</b><b>a</b><b>b</b>',
+ );
});
it('should return project name as it is if `matcher` is not available', () => {
- vm.matcher = null;
+ wrapper.setProps({ matcher: null });
- expect(vm.highlightedItemName).toBe(mockProject.name);
+ expect(trimText(wrapper.find('.js-frequent-items-item-title').text())).toBe(
+ mockProject.name,
+ );
});
});
describe('truncatedNamespace', () => {
it('should truncate project name from namespace string', () => {
- vm.namespace = 'platform / nokia-3310';
+ wrapper.setProps({ namespace: 'platform / nokia-3310' });
- expect(vm.truncatedNamespace).toBe('platform');
+ expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe('platform');
});
it('should truncate namespace string from the middle if it includes more than two groups in path', () => {
- vm.namespace = 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310';
+ wrapper.setProps({
+ namespace: 'platform / hardware / broadcom / Wifi Group / Mobile Chipset / nokia-3310',
+ });
- expect(vm.truncatedNamespace).toBe('platform / ... / Mobile Chipset');
+ expect(trimText(wrapper.find('.js-frequent-items-item-namespace').text())).toBe(
+ 'platform / ... / Mobile Chipset',
+ );
});
});
});
describe('template', () => {
it('should render component element', () => {
- expect(vm.$el.classList.contains('frequent-items-list-item-container')).toBeTruthy();
- expect(vm.$el.querySelectorAll('a').length).toBe(1);
- expect(vm.$el.querySelectorAll('.frequent-items-item-avatar-container').length).toBe(1);
- expect(vm.$el.querySelectorAll('.frequent-items-item-metadata-container').length).toBe(1);
- expect(vm.$el.querySelectorAll('.frequent-items-item-title').length).toBe(1);
- expect(vm.$el.querySelectorAll('.frequent-items-item-namespace').length).toBe(1);
+ expect(wrapper.classes()).toContain('frequent-items-list-item-container');
+ expect(wrapper.findAll('a').length).toBe(1);
+ expect(wrapper.findAll('.frequent-items-item-avatar-container').length).toBe(1);
+ expect(wrapper.findAll('.frequent-items-item-metadata-container').length).toBe(1);
+ expect(wrapper.findAll('.frequent-items-item-title').length).toBe(1);
+ expect(wrapper.findAll('.frequent-items-item-namespace').length).toBe(1);
});
});
});
diff --git a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
index d564292f1ba..ddbbc5c2d29 100644
--- a/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/javascripts/frequent_items/components/frequent_items_search_input_spec.js
@@ -1,19 +1,22 @@
import Vue from 'vue';
import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
import eventHub from '~/frequent_items/event_hub';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
const createComponent = (namespace = 'projects') => {
const Component = Vue.extend(searchComponent);
- return mountComponent(Component, { namespace });
+ return shallowMount(Component, { propsData: { namespace } });
};
describe('FrequentItemsSearchInputComponent', () => {
+ let wrapper;
let vm;
beforeEach(() => {
- vm = createComponent();
+ wrapper = createComponent();
+
+ ({ vm } = wrapper);
});
afterEach(() => {
@@ -35,7 +38,7 @@ describe('FrequentItemsSearchInputComponent', () => {
describe('mounted', () => {
it('should listen `dropdownOpen` event', done => {
spyOn(eventHub, '$on');
- const vmX = createComponent();
+ const vmX = createComponent().vm;
Vue.nextTick(() => {
expect(eventHub.$on).toHaveBeenCalledWith(
@@ -49,7 +52,7 @@ describe('FrequentItemsSearchInputComponent', () => {
describe('beforeDestroy', () => {
it('should unbind event listeners on eventHub', done => {
- const vmX = createComponent();
+ const vmX = createComponent().vm;
spyOn(eventHub, '$off');
vmX.$mount();
@@ -67,12 +70,12 @@ describe('FrequentItemsSearchInputComponent', () => {
describe('template', () => {
it('should render component element', () => {
- const inputEl = vm.$el.querySelector('input.form-control');
-
- expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
- expect(inputEl).not.toBe(null);
- expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
- expect(vm.$el.querySelector('.search-icon')).toBeDefined();
+ expect(wrapper.classes()).toContain('search-input-container');
+ expect(wrapper.contains('input.form-control')).toBe(true);
+ expect(wrapper.contains('.search-icon')).toBe(true);
+ expect(wrapper.find('input.form-control').attributes('placeholder')).toBe(
+ 'Search your projects',
+ );
});
});
});
diff --git a/spec/javascripts/gl_dropdown_spec.js b/spec/javascripts/gl_dropdown_spec.js
index 85083653db8..8c7820ddb52 100644
--- a/spec/javascripts/gl_dropdown_spec.js
+++ b/spec/javascripts/gl_dropdown_spec.js
@@ -5,8 +5,8 @@ import GLDropdown from '~/gl_dropdown';
import '~/lib/utils/common_utils';
describe('glDropdown', function describeDropdown() {
- preloadFixtures('static/gl_dropdown.html.raw');
- loadJSONFixtures('projects.json');
+ preloadFixtures('static/gl_dropdown.html');
+ loadJSONFixtures('static/projects.json');
const NON_SELECTABLE_CLASSES =
'.divider, .separator, .dropdown-header, .dropdown-menu-empty-item';
@@ -64,10 +64,10 @@ describe('glDropdown', function describeDropdown() {
}
beforeEach(() => {
- loadFixtures('static/gl_dropdown.html.raw');
+ loadFixtures('static/gl_dropdown.html');
this.dropdownContainerElement = $('.dropdown.inline');
this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = getJSONFixture('projects.json');
+ this.projectsData = getJSONFixture('static/projects.json');
});
afterEach(() => {
diff --git a/spec/javascripts/gl_field_errors_spec.js b/spec/javascripts/gl_field_errors_spec.js
index b463c9afbee..294f219d6fe 100644
--- a/spec/javascripts/gl_field_errors_spec.js
+++ b/spec/javascripts/gl_field_errors_spec.js
@@ -4,10 +4,10 @@ import $ from 'jquery';
import GlFieldErrors from '~/gl_field_errors';
describe('GL Style Field Errors', function() {
- preloadFixtures('static/gl_field_errors.html.raw');
+ preloadFixtures('static/gl_field_errors.html');
beforeEach(function() {
- loadFixtures('static/gl_field_errors.html.raw');
+ loadFixtures('static/gl_field_errors.html');
const $form = $('form.gl-show-field-errors');
this.$form = $form;
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index d832441dc93..31873311e16 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -502,7 +502,7 @@ describe('AppComponent', () => {
vm.isLoading = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.loading-animation')).toBeDefined();
- expect(vm.$el.querySelector('i.fa').getAttribute('aria-label')).toBe('Loading groups');
+ expect(vm.$el.querySelector('span').getAttribute('aria-label')).toBe('Loading groups');
done();
});
});
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
index 2fe34e5a76f..0ddf589f368 100644
--- a/spec/javascripts/header_spec.js
+++ b/spec/javascripts/header_spec.js
@@ -3,7 +3,7 @@ import initTodoToggle from '~/header';
describe('Header', function() {
const todosPendingCount = '.todos-count';
- const fixtureTemplate = 'issues/open-issue.html.raw';
+ const fixtureTemplate = 'issues/open-issue.html';
function isTodosCountHidden() {
return $(todosPendingCount).hasClass('hidden');
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
index 8933dd5def4..fd06bb1f324 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -5,7 +5,7 @@ export default class FilteredSearchSpecHelper {
static createFilterVisualToken(name, value, isSelected = false) {
const li = document.createElement('li');
- li.classList.add('js-visual-token', 'filtered-search-token');
+ li.classList.add('js-visual-token', 'filtered-search-token', `search-token-${name}`);
li.innerHTML = `
<div class="selectable ${isSelected ? 'selected' : ''}" role="button">
diff --git a/spec/javascripts/helpers/text_helper.js b/spec/javascripts/helpers/text_helper.js
new file mode 100644
index 00000000000..e0fe18e5560
--- /dev/null
+++ b/spec/javascripts/helpers/text_helper.js
@@ -0,0 +1,18 @@
+/**
+ * Replaces line break with an empty space
+ * @param {*} data
+ */
+export const removeBreakLine = data => data.replace(/\r?\n|\r/g, ' ');
+
+/**
+ * Removes line breaks, spaces and trims the given text
+ * @param {String} str
+ * @returns {String}
+ */
+export const trimText = str =>
+ str
+ .replace(/\r?\n|\r/g, '')
+ .replace(/\s\s+/g, ' ')
+ .trim();
+
+export const removeWhitespace = str => str.replace(/\s\s+/g, ' ');
diff --git a/spec/javascripts/helpers/vue_test_utils_helper.js b/spec/javascripts/helpers/vue_test_utils_helper.js
index 19e27388eeb..121e99c9783 100644
--- a/spec/javascripts/helpers/vue_test_utils_helper.js
+++ b/spec/javascripts/helpers/vue_test_utils_helper.js
@@ -16,4 +16,6 @@ const vNodeContainsText = (vnode, text) =>
* @param {String} text
*/
export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =>
- !!shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length;
+ Boolean(
+ shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length,
+ );
diff --git a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
index 3a5d6c8a90b..b903abe63fc 100644
--- a/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/actions_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import store from '~/ide/stores';
+import consts from '~/ide/stores/modules/commit/constants';
import commitActions from '~/ide/components/commit_sidebar/actions.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from 'spec/ide/helpers';
@@ -7,20 +8,33 @@ import { projectData } from 'spec/ide/mock_data';
describe('IDE commit sidebar actions', () => {
let vm;
-
- beforeEach(done => {
+ const createComponent = ({
+ hasMR = false,
+ commitAction = consts.COMMIT_TO_NEW_BRANCH,
+ mergeRequestsEnabled = true,
+ currentBranchId = 'master',
+ shouldCreateMR = false,
+ } = {}) => {
const Component = Vue.extend(commitActions);
vm = createComponentWithStore(Component, store);
- vm.$store.state.currentBranchId = 'master';
+ vm.$store.state.currentBranchId = currentBranchId;
vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.commit.commitAction = commitAction;
Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData });
+ vm.$store.state.projects.abcproject.merge_requests_enabled = mergeRequestsEnabled;
+ vm.$store.state.commit.shouldCreateMR = shouldCreateMR;
- vm.$mount();
+ if (hasMR) {
+ vm.$store.state.currentMergeRequestId = '1';
+ vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
+ store.state.currentMergeRequestId
+ ] = { foo: 'bar' };
+ }
- Vue.nextTick(done);
- });
+ return vm.$mount();
+ };
afterEach(() => {
vm.$destroy();
@@ -28,16 +42,20 @@ describe('IDE commit sidebar actions', () => {
resetStore(vm.$store);
});
- it('renders 3 groups', () => {
- expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(3);
+ it('renders 2 groups', () => {
+ createComponent();
+
+ expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2);
});
it('renders current branch text', () => {
+ createComponent();
+
expect(vm.$el.textContent).toContain('Commit to master branch');
});
it('hides merge request option when project merge requests are disabled', done => {
- vm.$store.state.projects.abcproject.merge_requests_enabled = false;
+ createComponent({ mergeRequestsEnabled: false });
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('input[type="radio"]').length).toBe(2);
@@ -49,9 +67,10 @@ describe('IDE commit sidebar actions', () => {
describe('commitToCurrentBranchText', () => {
it('escapes current branch', () => {
- vm.$store.state.currentBranchId = '<img src="x" />';
+ const injectedSrc = '<img src="x" />';
+ createComponent({ currentBranchId: injectedSrc });
- expect(vm.commitToCurrentBranchText).not.toContain('<img src="x" />');
+ expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc);
});
});
});
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
index 9af3c15a4e3..3c7d6192e2c 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -3,7 +3,7 @@ import store from '~/ide/stores';
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { file } from '../../helpers';
-import { removeWhitespace } from '../../../helpers/vue_component_helper';
+import { removeWhitespace } from '../../../helpers/text_helper';
describe('Multi-file editor commit sidebar list collapsed', () => {
let vm;
diff --git a/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js
new file mode 100644
index 00000000000..7017bfcd6a6
--- /dev/null
+++ b/spec/javascripts/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -0,0 +1,73 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import consts from '~/ide/stores/modules/commit/constants';
+import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { projectData } from 'spec/ide/mock_data';
+import { resetStore } from 'spec/ide/helpers';
+
+describe('create new MR checkbox', () => {
+ let vm;
+ const createComponent = ({
+ hasMR = false,
+ commitAction = consts.COMMIT_TO_NEW_BRANCH,
+ currentBranchId = 'master',
+ } = {}) => {
+ const Component = Vue.extend(NewMergeRequestOption);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.currentBranchId = currentBranchId;
+ vm.$store.state.currentProjectId = 'abcproject';
+ vm.$store.state.commit.commitAction = commitAction;
+ Vue.set(vm.$store.state.projects, 'abcproject', { ...projectData });
+
+ if (hasMR) {
+ vm.$store.state.currentMergeRequestId = '1';
+ vm.$store.state.projects[store.state.currentProjectId].mergeRequests[
+ store.state.currentMergeRequestId
+ ] = { foo: 'bar' };
+ }
+
+ return vm.$mount();
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('is hidden when an MR already exists and committing to current branch', () => {
+ createComponent({
+ hasMR: true,
+ commitAction: consts.COMMIT_TO_CURRENT_BRANCH,
+ currentBranchId: 'feature',
+ });
+
+ expect(vm.$el.textContent).toBe('');
+ });
+
+ it('does not hide checkbox if MR does not exist', () => {
+ createComponent({ hasMR: false });
+
+ expect(vm.$el.querySelector('input[type="checkbox"]').hidden).toBe(false);
+ });
+
+ it('does not hide checkbox when creating a new branch', () => {
+ createComponent({ commitAction: consts.COMMIT_TO_NEW_BRANCH });
+
+ expect(vm.$el.querySelector('input[type="checkbox"]').hidden).toBe(false);
+ });
+
+ it('dispatches toggleShouldCreateMR when clicking checkbox', () => {
+ createComponent();
+ const el = vm.$el.querySelector('input[type="checkbox"]');
+ spyOn(vm.$store, 'dispatch');
+ el.dispatchEvent(new Event('change'));
+
+ expect(vm.$store.dispatch.calls.allArgs()).toEqual(
+ jasmine.arrayContaining([['commit/toggleShouldCreateMR', jasmine.any(Object)]]),
+ );
+ });
+});
diff --git a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
index ffc2a4c9ddb..db1988be3e1 100644
--- a/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/radio_group_spec.js
@@ -76,6 +76,7 @@ describe('IDE commit sidebar radio group', () => {
const Component = Vue.extend(radioGroup);
store.state.commit.commitAction = '1';
+ store.state.commit.newBranchName = 'test-123';
vm = createComponentWithStore(Component, store, {
value: '1',
@@ -113,6 +114,12 @@ describe('IDE commit sidebar radio group', () => {
done();
});
});
+
+ it('renders newBranchName if present', () => {
+ const input = vm.$el.querySelector('.form-control');
+
+ expect(input.value).toBe('test-123');
+ });
});
describe('tooltipTitle', () => {
diff --git a/spec/javascripts/ide/components/error_message_spec.js b/spec/javascripts/ide/components/error_message_spec.js
index 430e8e2baa3..80d6c7fd564 100644
--- a/spec/javascripts/ide/components/error_message_spec.js
+++ b/spec/javascripts/ide/components/error_message_spec.js
@@ -84,7 +84,7 @@ describe('IDE error message component', () => {
expect(vm.isLoading).toBe(true);
- vm.$nextTick(() => {
+ setTimeout(() => {
expect(vm.isLoading).toBe(false);
done();
diff --git a/spec/javascripts/ide/components/file_row_extra_spec.js b/spec/javascripts/ide/components/file_row_extra_spec.js
index c93a939ad71..d7fed3f0681 100644
--- a/spec/javascripts/ide/components/file_row_extra_spec.js
+++ b/spec/javascripts/ide/components/file_row_extra_spec.js
@@ -20,7 +20,7 @@ describe('IDE extra file row component', () => {
file: {
...file('test'),
},
- mouseOver: false,
+ dropdownOpen: false,
});
spyOnProperty(vm, 'getUnstagedFilesCountForPath').and.returnValue(() => unstagedFilesCount);
diff --git a/spec/javascripts/ide/components/ide_review_spec.js b/spec/javascripts/ide/components/ide_review_spec.js
index b9ee22b7c1a..396c5d282d4 100644
--- a/spec/javascripts/ide/components/ide_review_spec.js
+++ b/spec/javascripts/ide/components/ide_review_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import IdeReview from '~/ide/components/ide_review.vue';
import store from '~/ide/stores';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { trimText } from '../../helpers/vue_component_helper';
+import { trimText } from '../../helpers/text_helper';
import { resetStore, file } from '../helpers';
import { projectData } from '../mock_data';
diff --git a/spec/javascripts/ide/components/ide_spec.js b/spec/javascripts/ide/components/ide_spec.js
index dc5790f6562..de4becec1cd 100644
--- a/spec/javascripts/ide/components/ide_spec.js
+++ b/spec/javascripts/ide/components/ide_spec.js
@@ -5,21 +5,53 @@ import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helpe
import { file, resetStore } from '../helpers';
import { projectData } from '../mock_data';
-describe('ide component', () => {
+function bootstrap(projData) {
+ const Component = Vue.extend(ide);
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = Object.assign({}, projData);
+ Vue.set(store.state.trees, 'abcproject/master', {
+ tree: [],
+ loading: false,
+ });
+
+ return createComponentWithStore(Component, store, {
+ emptyStateSvgPath: 'svg',
+ noChangesStateSvgPath: 'svg',
+ committedStateSvgPath: 'svg',
+ });
+}
+
+describe('ide component, empty repo', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(ide);
+ const emptyProjData = Object.assign({}, projectData, { empty_repo: true, branches: {} });
+ vm = bootstrap(emptyProjData);
+ vm.$mount();
+ });
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = Object.assign({}, projectData);
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
- vm = createComponentWithStore(Component, store, {
- emptyStateSvgPath: 'svg',
- noChangesStateSvgPath: 'svg',
- committedStateSvgPath: 'svg',
- }).$mount();
+ it('renders "New file" button in empty repo', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).not.toBeNull();
+ done();
+ });
+ });
+});
+
+describe('ide component, non-empty repo', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = bootstrap(projectData);
+ vm.$mount();
});
afterEach(() => {
@@ -28,17 +60,15 @@ describe('ide component', () => {
resetStore(vm.$store);
});
- it('does not render right when no files open', () => {
- expect(vm.$el.querySelector('.panel-right')).toBeNull();
- });
+ it('shows error message when set', done => {
+ expect(vm.$el.querySelector('.flash-container')).toBe(null);
- it('renders right panel when files are open', done => {
- vm.$store.state.trees['abcproject/mybranch'] = {
- tree: [file()],
+ vm.$store.state.errorMessage = {
+ text: 'error',
};
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.panel-right')).toBeNull();
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
done();
});
@@ -71,17 +101,25 @@ describe('ide component', () => {
});
});
- it('shows error message when set', done => {
- expect(vm.$el.querySelector('.flash-container')).toBe(null);
-
- vm.$store.state.errorMessage = {
- text: 'error',
- };
+ describe('non-existent branch', () => {
+ it('does not render "New file" button for non-existent branch when repo is not empty', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
+ done();
+ });
+ });
+ });
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
+ describe('branch with files', () => {
+ beforeEach(() => {
+ store.state.trees['abcproject/master'].tree = [file()];
+ });
- done();
+ it('does not render "New file" button', done => {
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.ide-empty-state button[title="New file"]')).toBeNull();
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/ide/components/ide_tree_list_spec.js b/spec/javascripts/ide/components/ide_tree_list_spec.js
index 4ecbdb8a55e..f63007c7dd2 100644
--- a/spec/javascripts/ide/components/ide_tree_list_spec.js
+++ b/spec/javascripts/ide/components/ide_tree_list_spec.js
@@ -7,25 +7,23 @@ import { projectData } from '../mock_data';
describe('IDE tree list', () => {
const Component = Vue.extend(IdeTreeList);
+ const normalBranchTree = [file('fileName')];
+ const emptyBranchTree = [];
let vm;
- beforeEach(() => {
+ const bootstrapWithTree = (tree = normalBranchTree) => {
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
store.state.projects.abcproject = Object.assign({}, projectData);
Vue.set(store.state.trees, 'abcproject/master', {
- tree: [file('fileName')],
+ tree,
loading: false,
});
vm = createComponentWithStore(Component, store, {
viewerType: 'edit',
});
-
- spyOn(vm, 'updateViewer').and.callThrough();
-
- vm.$mount();
- });
+ };
afterEach(() => {
vm.$destroy();
@@ -33,22 +31,47 @@ describe('IDE tree list', () => {
resetStore(vm.$store);
});
- it('updates viewer on mount', () => {
- expect(vm.updateViewer).toHaveBeenCalledWith('edit');
- });
+ describe('normal branch', () => {
+ beforeEach(() => {
+ bootstrapWithTree();
+
+ spyOn(vm, 'updateViewer').and.callThrough();
+
+ vm.$mount();
+ });
+
+ it('updates viewer on mount', () => {
+ expect(vm.updateViewer).toHaveBeenCalledWith('edit');
+ });
+
+ it('renders loading indicator', done => {
+ store.state.trees['abcproject/master'].loading = true;
- it('renders loading indicator', done => {
- store.state.trees['abcproject/master'].loading = true;
+ vm.$nextTick(() => {
+ expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
+ expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.multi-file-loading-container')).not.toBeNull();
- expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toBe(3);
+ done();
+ });
+ });
- done();
+ it('renders list of files', () => {
+ expect(vm.$el.textContent).toContain('fileName');
});
});
- it('renders list of files', () => {
- expect(vm.$el.textContent).toContain('fileName');
+ describe('empty-branch state', () => {
+ beforeEach(() => {
+ bootstrapWithTree(emptyBranchTree);
+
+ spyOn(vm, 'updateViewer').and.callThrough();
+
+ vm.$mount();
+ });
+
+ it('does not load files if the branch is empty', () => {
+ expect(vm.$el.textContent).not.toContain('fileName');
+ expect(vm.$el.textContent).toContain('No files');
+ });
});
});
diff --git a/spec/javascripts/ide/components/nav_dropdown_button_spec.js b/spec/javascripts/ide/components/nav_dropdown_button_spec.js
index 0a58e260280..19b0071567a 100644
--- a/spec/javascripts/ide/components/nav_dropdown_button_spec.js
+++ b/spec/javascripts/ide/components/nav_dropdown_button_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import NavDropdownButton from '~/ide/components/nav_dropdown_button.vue';
import store from '~/ide/stores';
-import { trimText } from 'spec/helpers/vue_component_helper';
+import { trimText } from 'spec/helpers/text_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
diff --git a/spec/javascripts/ide/components/new_dropdown/index_spec.js b/spec/javascripts/ide/components/new_dropdown/index_spec.js
index 83e530f0a6a..aaebe88f314 100644
--- a/spec/javascripts/ide/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/index_spec.js
@@ -56,11 +56,11 @@ describe('new dropdown component', () => {
});
});
- describe('dropdownOpen', () => {
+ describe('isOpen', () => {
it('scrolls dropdown into view', done => {
spyOn(vm.$refs.dropdownMenu, 'scrollIntoView');
- vm.dropdownOpen = true;
+ vm.isOpen = true;
setTimeout(() => {
expect(vm.$refs.dropdownMenu.scrollIntoView).toHaveBeenCalledWith({
diff --git a/spec/javascripts/ide/components/new_dropdown/modal_spec.js b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
index d94cc1a8faa..0556feae46a 100644
--- a/spec/javascripts/ide/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/modal_spec.js
@@ -18,6 +18,9 @@ describe('new file modal component', () => {
store.state.entryModal = {
type,
path: '',
+ entry: {
+ path: '',
+ },
};
vm = createComponentWithStore(Component, store).$mount();
@@ -74,6 +77,7 @@ describe('new file modal component', () => {
entry: {
name: 'test',
type: 'blob',
+ path: 'test-path',
},
};
@@ -97,7 +101,7 @@ describe('new file modal component', () => {
describe('entryName', () => {
it('returns entries name', () => {
- expect(vm.entryName).toBe('test');
+ expect(vm.entryName).toBe('test-path');
});
it('updated name', () => {
@@ -105,6 +109,67 @@ describe('new file modal component', () => {
expect(vm.entryName).toBe('index.js');
});
+
+ it('removes leading/trailing spaces when found in the new name', () => {
+ vm.entryName = ' index.js ';
+
+ expect(vm.entryName).toBe('index.js');
+ });
+
+ it('does not remove internal spaces in the file name', () => {
+ vm.entryName = ' In Praise of Idleness.txt ';
+
+ expect(vm.entryName).toBe('In Praise of Idleness.txt');
+ });
+ });
+ });
+
+ describe('submitForm', () => {
+ it('throws an error when target entry exists', () => {
+ const store = createStore();
+ store.state.entryModal = {
+ type: 'rename',
+ path: 'test-path/test',
+ entry: {
+ name: 'test',
+ type: 'blob',
+ path: 'test-path/test',
+ },
+ };
+ store.state.entries = {
+ 'test-path/test': {
+ name: 'test',
+ deleted: false,
+ },
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
+ const flashSpy = spyOnDependency(modal, 'flash');
+ vm.submitForm();
+
+ expect(flashSpy).toHaveBeenCalled();
+ });
+
+ it('calls createTempEntry when target path does not exist', () => {
+ const store = createStore();
+ store.state.entryModal = {
+ type: 'rename',
+ path: 'test-path/test',
+ entry: {
+ name: 'test',
+ type: 'blob',
+ path: 'test-path1/test',
+ },
+ };
+
+ vm = createComponentWithStore(Component, store).$mount();
+ spyOn(vm, 'createTempEntry').and.callFake(() => Promise.resolve());
+ vm.submitForm();
+
+ expect(vm.createTempEntry).toHaveBeenCalledWith({
+ name: 'test-path1',
+ type: 'tree',
+ });
});
});
});
diff --git a/spec/javascripts/ide/components/new_dropdown/upload_spec.js b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
index 878e17ac805..d19af6af2d7 100644
--- a/spec/javascripts/ide/components/new_dropdown/upload_spec.js
+++ b/spec/javascripts/ide/components/new_dropdown/upload_spec.js
@@ -78,6 +78,8 @@ describe('new dropdown upload', () => {
type: 'blob',
content: 'plain text',
base64: false,
+ binary: false,
+ rawPath: '',
});
});
@@ -89,6 +91,8 @@ describe('new dropdown upload', () => {
type: 'blob',
content: binaryTarget.result.split('base64,')[1],
base64: true,
+ binary: true,
+ rawPath: binaryTarget.result,
});
});
});
diff --git a/spec/javascripts/ide/components/pipelines/list_spec.js b/spec/javascripts/ide/components/pipelines/list_spec.js
index 68487733cb9..80829f2358a 100644
--- a/spec/javascripts/ide/components/pipelines/list_spec.js
+++ b/spec/javascripts/ide/components/pipelines/list_spec.js
@@ -11,6 +11,8 @@ describe('IDE pipelines list', () => {
let vm;
let mock;
+ const findLoadingState = () => vm.$el.querySelector('.loading-container');
+
beforeEach(done => {
const store = createStore();
@@ -95,7 +97,7 @@ describe('IDE pipelines list', () => {
describe('empty state', () => {
it('renders pipelines empty state', done => {
- vm.$store.state.pipelines.latestPipeline = false;
+ vm.$store.state.pipelines.latestPipeline = null;
vm.$nextTick(() => {
expect(vm.$el.querySelector('.empty-state')).not.toBe(null);
@@ -106,15 +108,30 @@ describe('IDE pipelines list', () => {
});
describe('loading state', () => {
- it('renders loading state when there is no latest pipeline', done => {
- vm.$store.state.pipelines.latestPipeline = null;
+ beforeEach(() => {
vm.$store.state.pipelines.isLoadingPipeline = true;
+ });
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+ it('does not render when pipeline has loaded before', done => {
+ vm.$store.state.pipelines.hasLoadedPipeline = true;
- done();
- });
+ vm.$nextTick()
+ .then(() => {
+ expect(findLoadingState()).toBe(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('renders loading state when there is no latest pipeline', done => {
+ vm.$store.state.pipelines.hasLoadedPipeline = false;
+
+ vm.$nextTick()
+ .then(() => {
+ expect(findLoadingState()).not.toBe(null);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/ide/lib/diff/diff_spec.js b/spec/javascripts/ide/lib/diff/diff_spec.js
deleted file mode 100644
index 57f3ac3d365..00000000000
--- a/spec/javascripts/ide/lib/diff/diff_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import { computeDiff } from '~/ide/lib/diff/diff';
-
-describe('Multi-file editor library diff calculator', () => {
- describe('computeDiff', () => {
- it('returns empty array if no changes', () => {
- const diff = computeDiff('123', '123');
-
- expect(diff).toEqual([]);
- });
-
- describe('modified', () => {
- it('', () => {
- const diff = computeDiff('123', '1234')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeUndefined();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeUndefined();
- expect(diff.lineNumber).toBe(2);
- });
- });
-
- describe('added', () => {
- it('', () => {
- const diff = computeDiff('123', '123\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeUndefined();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeUndefined();
- expect(diff.lineNumber).toBe(3);
- });
- });
-
- describe('removed', () => {
- it('', () => {
- const diff = computeDiff('123', '')[0];
-
- expect(diff.added).toBeUndefined();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeTruthy();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n123')[0];
-
- expect(diff.added).toBeUndefined();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeTruthy();
- expect(diff.lineNumber).toBe(2);
- });
- });
-
- it('includes line number of change', () => {
- const diff = computeDiff('123', '')[0];
-
- expect(diff.lineNumber).toBe(1);
- });
-
- it('includes end line number of change', () => {
- const diff = computeDiff('123', '')[0];
-
- expect(diff.endLineNumber).toBe(1);
- });
- });
-});
diff --git a/spec/javascripts/ide/mock_data.js b/spec/javascripts/ide/mock_data.js
index 4fe826943b2..570a396c5e3 100644
--- a/spec/javascripts/ide/mock_data.js
+++ b/spec/javascripts/ide/mock_data.js
@@ -16,6 +16,7 @@ export const projectData = {
},
mergeRequests: {},
merge_requests_enabled: true,
+ default_branch: 'master',
};
export const pipelines = [
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 7ddc734ff56..dd2313dc800 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -10,11 +10,19 @@ import eventHub from '~/ide/eventhub';
import { file, resetStore } from '../../helpers';
import testAction from '../../../helpers/vuex_action_helper';
+const RELATIVE_URL_ROOT = '/gitlab';
+
describe('IDE store file actions', () => {
let mock;
+ let originalGon;
beforeEach(() => {
mock = new MockAdapter(axios);
+ originalGon = window.gon;
+ window.gon = {
+ ...window.gon,
+ relative_url_root: RELATIVE_URL_ROOT,
+ };
spyOn(router, 'push');
});
@@ -22,6 +30,7 @@ describe('IDE store file actions', () => {
afterEach(() => {
mock.restore();
resetStore(store);
+ window.gon = originalGon;
});
describe('closeFile', () => {
@@ -121,68 +130,48 @@ describe('IDE store file actions', () => {
store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
});
- it('calls scrollToTab', done => {
- store
- .dispatch('setFileActive', localFile.path)
- .then(() => {
- expect(scrollToTabSpy).toHaveBeenCalled();
+ it('calls scrollToTab', () => {
+ const dispatch = jasmine.createSpy();
- done();
- })
- .catch(done.fail);
- });
-
- it('sets the file active', done => {
- store
- .dispatch('setFileActive', localFile.path)
- .then(() => {
- expect(localFile.active).toBeTruthy();
+ actions.setFileActive(
+ { commit() {}, state: store.state, getters: store.getters, dispatch },
+ localFile.path,
+ );
- done();
- })
- .catch(done.fail);
+ expect(dispatch).toHaveBeenCalledWith('scrollToTab');
});
- it('returns early if file is already active', done => {
- localFile.active = true;
+ it('commits SET_FILE_ACTIVE', () => {
+ const commit = jasmine.createSpy();
- store
- .dispatch('setFileActive', localFile.path)
- .then(() => {
- expect(scrollToTabSpy).not.toHaveBeenCalled();
+ actions.setFileActive(
+ { commit, state: store.state, getters: store.getters, dispatch() {} },
+ localFile.path,
+ );
- done();
- })
- .catch(done.fail);
+ expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', {
+ path: localFile.path,
+ active: true,
+ });
});
- it('sets current active file to not active', done => {
+ it('sets current active file to not active', () => {
const f = file('newActive');
store.state.entries[f.path] = f;
localFile.active = true;
store.state.openFiles.push(localFile);
- store
- .dispatch('setFileActive', f.path)
- .then(() => {
- expect(localFile.active).toBeFalsy();
+ const commit = jasmine.createSpy();
- done();
- })
- .catch(done.fail);
- });
-
- it('resets location.hash for line highlighting', done => {
- window.location.hash = 'test';
-
- store
- .dispatch('setFileActive', localFile.path)
- .then(() => {
- expect(window.location.hash).not.toBe('test');
+ actions.setFileActive(
+ { commit, state: store.state, getters: store.getters, dispatch() {} },
+ f.path,
+ );
- done();
- })
- .catch(done.fail);
+ expect(commit).toHaveBeenCalledWith('SET_FILE_ACTIVE', {
+ path: localFile.path,
+ active: false,
+ });
});
});
@@ -193,13 +182,13 @@ describe('IDE store file actions', () => {
spyOn(service, 'getFileData').and.callThrough();
localFile = file(`newCreate-${Math.random()}`);
- localFile.url = `${gl.TEST_HOST}/getFileDataURL`;
+ localFile.url = `project/getFileDataURL`;
store.state.entries[localFile.path] = localFile;
});
describe('success', () => {
beforeEach(() => {
- mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).replyOnce(
+ mock.onGet(`${RELATIVE_URL_ROOT}/project/getFileDataURL`).replyOnce(
200,
{
blame_path: 'blame_path',
@@ -220,7 +209,9 @@ describe('IDE store file actions', () => {
store
.dispatch('getFileData', { path: localFile.path })
.then(() => {
- expect(service.getFileData).toHaveBeenCalledWith(`${gl.TEST_HOST}/getFileDataURL`);
+ expect(service.getFileData).toHaveBeenCalledWith(
+ `${RELATIVE_URL_ROOT}/project/getFileDataURL`,
+ );
done();
})
@@ -286,7 +277,7 @@ describe('IDE store file actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${gl.TEST_HOST}/getFileDataURL`).networkError();
+ mock.onGet(`project/getFileDataURL`).networkError();
});
it('dispatches error action', done => {
@@ -728,4 +719,20 @@ describe('IDE store file actions', () => {
.catch(done.fail);
});
});
+
+ describe('triggerFilesChange', () => {
+ beforeEach(() => {
+ spyOn(eventHub, '$emit');
+ });
+
+ it('emits event that files have changed', done => {
+ store
+ .dispatch('triggerFilesChange')
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('ide.files.change');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
index 9bfc7c397b8..4dd0c1150eb 100644
--- a/spec/javascripts/ide/stores/actions/merge_request_spec.js
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -11,13 +11,17 @@ import service from '~/ide/services';
import { activityBarViews } from '~/ide/constants';
import { resetStore } from '../../helpers';
+const TEST_PROJECT = 'abcproject';
+const TEST_PROJECT_ID = 17;
+
describe('IDE store merge request actions', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- store.state.projects.abcproject = {
+ store.state.projects[TEST_PROJECT] = {
+ id: TEST_PROJECT_ID,
mergeRequests: {},
};
});
@@ -27,6 +31,98 @@ describe('IDE store merge request actions', () => {
resetStore(store);
});
+ describe('getMergeRequestsForBranch', () => {
+ describe('success', () => {
+ const mrData = { iid: 2, source_branch: 'bar' };
+ const mockData = [mrData];
+
+ describe('base case', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequests').and.callThrough();
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, mockData);
+ });
+
+ it('calls getProjectMergeRequests service method', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ expect(service.getProjectMergeRequests).toHaveBeenCalledWith(TEST_PROJECT, {
+ source_branch: 'bar',
+ source_project_id: TEST_PROJECT_ID,
+ order_by: 'created_at',
+ per_page: 1,
+ });
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets the "Merge Request" Object', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ expect(store.state.projects.abcproject.mergeRequests).toEqual({
+ '2': jasmine.objectContaining(mrData),
+ });
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('sets "Current Merge Request" object to the most recent MR', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ expect(store.state.currentMergeRequestId).toEqual('2');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('no merge requests for branch available case', () => {
+ beforeEach(() => {
+ spyOn(service, 'getProjectMergeRequests').and.callThrough();
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, []);
+ });
+
+ it('does not fail if there are no merge requests for current branch', done => {
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'foo' })
+ .then(() => {
+ expect(store.state.projects[TEST_PROJECT].mergeRequests).toEqual({});
+ expect(store.state.currentMergeRequestId).toEqual('');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('error', () => {
+ beforeEach(() => {
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).networkError();
+ });
+
+ it('flashes message, if error', done => {
+ const flashSpy = spyOnDependency(actions, 'flash');
+
+ store
+ .dispatch('getMergeRequestsForBranch', { projectId: TEST_PROJECT, branchId: 'bar' })
+ .then(() => {
+ fail('Expected getMergeRequestsForBranch to throw an error');
+ })
+ .catch(() => {
+ expect(flashSpy).toHaveBeenCalled();
+ expect(flashSpy.calls.argsFor(0)[0]).toEqual('Error fetching merge requests for bar');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
describe('getMergeRequestData', () => {
describe('success', () => {
beforeEach(() => {
@@ -39,9 +135,9 @@ describe('IDE store merge request actions', () => {
it('calls getProjectMergeRequestData service method', done => {
store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1, {
+ expect(service.getProjectMergeRequestData).toHaveBeenCalledWith(TEST_PROJECT, 1, {
render_html: true,
});
@@ -52,10 +148,12 @@ describe('IDE store merge request actions', () => {
it('sets the Merge Request Object', done => {
store
- .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestData', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
expect(store.state.currentMergeRequestId).toBe(1);
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].title).toBe(
+ 'mergerequest',
+ );
done();
})
@@ -77,7 +175,7 @@ describe('IDE store merge request actions', () => {
dispatch,
state: store.state,
},
- { projectId: 'abcproject', mergeRequestId: 1 },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
)
.then(done.fail)
.catch(() => {
@@ -86,7 +184,7 @@ describe('IDE store merge request actions', () => {
action: jasmine.any(Function),
actionText: 'Please try again',
actionPayload: {
- projectId: 'abcproject',
+ projectId: TEST_PROJECT,
mergeRequestId: 1,
force: false,
},
@@ -100,7 +198,7 @@ describe('IDE store merge request actions', () => {
describe('getMergeRequestChanges', () => {
beforeEach(() => {
- store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
+ store.state.projects[TEST_PROJECT].mergeRequests['1'] = { changes: [] };
});
describe('success', () => {
@@ -114,9 +212,9 @@ describe('IDE store merge request actions', () => {
it('calls getProjectMergeRequestChanges service method', done => {
store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
+ expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith(TEST_PROJECT, 1);
done();
})
@@ -125,9 +223,9 @@ describe('IDE store merge request actions', () => {
it('sets the Merge Request Changes Object', done => {
store
- .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestChanges', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].changes.title).toBe(
'mergerequest',
);
done();
@@ -150,7 +248,7 @@ describe('IDE store merge request actions', () => {
dispatch,
state: store.state,
},
- { projectId: 'abcproject', mergeRequestId: 1 },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
)
.then(done.fail)
.catch(() => {
@@ -159,7 +257,7 @@ describe('IDE store merge request actions', () => {
action: jasmine.any(Function),
actionText: 'Please try again',
actionPayload: {
- projectId: 'abcproject',
+ projectId: TEST_PROJECT,
mergeRequestId: 1,
force: false,
},
@@ -173,7 +271,7 @@ describe('IDE store merge request actions', () => {
describe('getMergeRequestVersions', () => {
beforeEach(() => {
- store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
+ store.state.projects[TEST_PROJECT].mergeRequests['1'] = { versions: [] };
});
describe('success', () => {
@@ -186,9 +284,9 @@ describe('IDE store merge request actions', () => {
it('calls getProjectMergeRequestVersions service method', done => {
store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
+ expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith(TEST_PROJECT, 1);
done();
})
@@ -197,9 +295,9 @@ describe('IDE store merge request actions', () => {
it('sets the Merge Request Versions Object', done => {
store
- .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+ .dispatch('getMergeRequestVersions', { projectId: TEST_PROJECT, mergeRequestId: 1 })
.then(() => {
- expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
+ expect(store.state.projects[TEST_PROJECT].mergeRequests['1'].versions.length).toBe(1);
done();
})
.catch(done.fail);
@@ -220,7 +318,7 @@ describe('IDE store merge request actions', () => {
dispatch,
state: store.state,
},
- { projectId: 'abcproject', mergeRequestId: 1 },
+ { projectId: TEST_PROJECT, mergeRequestId: 1 },
)
.then(done.fail)
.catch(() => {
@@ -229,7 +327,7 @@ describe('IDE store merge request actions', () => {
action: jasmine.any(Function),
actionText: 'Please try again',
actionPayload: {
- projectId: 'abcproject',
+ projectId: TEST_PROJECT,
mergeRequestId: 1,
force: false,
},
@@ -243,7 +341,7 @@ describe('IDE store merge request actions', () => {
describe('openMergeRequest', () => {
const mr = {
- projectId: 'abcproject',
+ projectId: TEST_PROJECT,
targetProjectId: 'defproject',
mergeRequestId: 2,
};
diff --git a/spec/javascripts/ide/stores/actions/project_spec.js b/spec/javascripts/ide/stores/actions/project_spec.js
index 7d8c9edd965..8ecb6129c63 100644
--- a/spec/javascripts/ide/stores/actions/project_spec.js
+++ b/spec/javascripts/ide/stores/actions/project_spec.js
@@ -4,7 +4,7 @@ import {
refreshLastCommitData,
showBranchNotFoundError,
createNewBranchFromDefault,
- getBranchData,
+ showEmptyState,
openBranch,
} from '~/ide/stores/actions';
import store from '~/ide/stores';
@@ -196,39 +196,44 @@ describe('IDE store project actions', () => {
});
});
- describe('getBranchData', () => {
- describe('error', () => {
- it('dispatches branch not found action when response is 404', done => {
- const dispatch = jasmine.createSpy('dispatchSpy');
-
- mock.onGet(/(.*)/).replyOnce(404);
-
- getBranchData(
+ describe('showEmptyState', () => {
+ it('commits proper mutations when supplied error is 404', done => {
+ testAction(
+ showEmptyState,
+ {
+ err: {
+ response: {
+ status: 404,
+ },
+ },
+ projectId: 'abc/def',
+ branchId: 'master',
+ },
+ store.state,
+ [
{
- commit() {},
- dispatch,
- state: store.state,
+ type: 'CREATE_TREE',
+ payload: {
+ treePath: 'abc/def/master',
+ },
},
{
- projectId: 'abc/def',
- branchId: 'master-testing',
+ type: 'TOGGLE_LOADING',
+ payload: {
+ entry: store.state.trees['abc/def/master'],
+ forceValue: false,
+ },
},
- )
- .then(done.fail)
- .catch(() => {
- expect(dispatch.calls.argsFor(0)).toEqual([
- 'showBranchNotFoundError',
- 'master-testing',
- ]);
- done();
- });
- });
+ ],
+ [],
+ done,
+ );
});
});
describe('openBranch', () => {
const branch = {
- projectId: 'feature/lorem-ipsum',
+ projectId: 'abc/def',
branchId: '123-lorem',
};
@@ -238,45 +243,113 @@ describe('IDE store project actions', () => {
'foo/bar-pending': { pending: true },
'foo/bar': { pending: false },
};
-
- spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
});
- it('dispatches branch actions', done => {
- openBranch(store, branch)
- .then(() => {
- expect(store.dispatch.calls.allArgs()).toEqual([
- ['setCurrentBranchId', branch.branchId],
- ['getBranchData', branch],
- ['getFiles', branch],
- ]);
- })
- .then(done)
- .catch(done.fail);
+ describe('empty repo', () => {
+ beforeEach(() => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
+
+ store.state.currentProjectId = 'abc/def';
+ store.state.projects['abc/def'] = {
+ empty_repo: true,
+ };
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ it('dispatches showEmptyState action right away', done => {
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['setCurrentBranchId', branch.branchId],
+ ['showEmptyState', branch],
+ ]);
+ done();
+ })
+ .catch(done.fail);
+ });
});
- it('handles tree entry action, if basePath is given', done => {
- openBranch(store, { ...branch, basePath: 'foo/bar/' })
- .then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- store.state.entries['foo/bar'],
- );
- })
- .then(done)
- .catch(done.fail);
+ describe('existing branch', () => {
+ beforeEach(() => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.resolve());
+ });
+
+ it('dispatches branch actions', done => {
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['setCurrentBranchId', branch.branchId],
+ ['getBranchData', branch],
+ ['getMergeRequestsForBranch', branch],
+ ['getFiles', branch],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('handles tree entry action, if basePath is given', done => {
+ openBranch(store, { ...branch, basePath: 'foo/bar/' })
+ .then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ store.state.entries['foo/bar'],
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not handle tree entry action, if entry is pending', done => {
+ openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
+ .then(() => {
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ jasmine.anything(),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('creates a new file supplied via URL if the file does not exist yet', done => {
+ openBranch(store, { ...branch, basePath: 'not-existent.md' })
+ .then(() => {
+ expect(store.dispatch).not.toHaveBeenCalledWith(
+ 'handleTreeEntryAction',
+ jasmine.anything(),
+ );
+
+ expect(store.dispatch).toHaveBeenCalledWith('createTempEntry', {
+ name: 'not-existent.md',
+ type: 'blob',
+ });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
- it('does not handle tree entry action, if entry is pending', done => {
- openBranch(store, { ...branch, basePath: 'foo/bar-pending' })
- .then(() => {
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'handleTreeEntryAction',
- jasmine.anything(),
- );
- })
- .then(done)
- .catch(done.fail);
+ describe('non-existent branch', () => {
+ beforeEach(() => {
+ spyOn(store, 'dispatch').and.returnValue(Promise.reject());
+ });
+
+ it('dispatches correct branch actions', done => {
+ openBranch(store, branch)
+ .then(() => {
+ expect(store.dispatch.calls.allArgs()).toEqual([
+ ['setCurrentBranchId', branch.branchId],
+ ['getBranchData', branch],
+ ['showBranchNotFoundError', branch.branchId],
+ ]);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index bd41e87bf0e..674ecdc6764 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'spec/helpers/vuex_action_helper';
-import { showTreeEntry, getFiles } from '~/ide/stores/actions/tree';
+import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
@@ -20,6 +20,7 @@ describe('Multi-file store tree actions', () => {
};
beforeEach(() => {
+ jasmine.clock().install();
spyOn(router, 'push');
mock = new MockAdapter(axios);
@@ -37,6 +38,7 @@ describe('Multi-file store tree actions', () => {
});
afterEach(() => {
+ jasmine.clock().uninstall();
mock.restore();
resetStore(store);
});
@@ -70,6 +72,11 @@ describe('Multi-file store tree actions', () => {
store
.dispatch('getFiles', basicCallParameters)
.then(() => {
+ // The populating of the tree is deferred for performance reasons.
+ // See this merge request for details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/25700
+ jasmine.clock().tick(1);
+ })
+ .then(() => {
projectTree = store.state.trees['abcproject/master'];
expect(projectTree.tree.length).toBe(2);
@@ -86,38 +93,6 @@ describe('Multi-file store tree actions', () => {
});
describe('error', () => {
- it('dispatches branch not found actions when response is 404', done => {
- const dispatch = jasmine.createSpy('dispatchSpy');
-
- store.state.projects = {
- 'abc/def': {
- web_url: `${gl.TEST_HOST}/files`,
- },
- };
-
- mock.onGet(/(.*)/).replyOnce(404);
-
- getFiles(
- {
- commit() {},
- dispatch,
- state: store.state,
- },
- {
- projectId: 'abc/def',
- branchId: 'master-testing',
- },
- )
- .then(done.fail)
- .catch(() => {
- expect(dispatch.calls.argsFor(0)).toEqual([
- 'showBranchNotFoundError',
- 'master-testing',
- ]);
- done();
- });
- });
-
it('dispatches error action', done => {
const dispatch = jasmine.createSpy('dispatchSpy');
@@ -199,4 +174,35 @@ describe('Multi-file store tree actions', () => {
);
});
});
+
+ describe('setDirectoryData', () => {
+ it('sets tree correctly if there are no opened files yet', done => {
+ const treeFile = file({ name: 'README.md' });
+ store.state.trees['abcproject/master'] = {};
+
+ testAction(
+ setDirectoryData,
+ { projectId: 'abcproject', branchId: 'master', treeList: [treeFile] },
+ store.state,
+ [
+ {
+ type: types.SET_DIRECTORY_DATA,
+ payload: {
+ treePath: 'abcproject/master',
+ data: [treeFile],
+ },
+ },
+ {
+ type: types.TOGGLE_LOADING,
+ payload: {
+ entry: {},
+ forceValue: false,
+ },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index df291ade3f7..37354283cab 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -9,12 +9,15 @@ import actions, {
setErrorMessage,
deleteEntry,
renameEntry,
+ getBranchData,
} from '~/ide/stores/actions';
+import axios from '~/lib/utils/axios_utils';
import store from '~/ide/stores';
import * as types from '~/ide/stores/mutation_types';
import router from '~/ide/ide_router';
import { resetStore, file } from '../helpers';
import testAction from '../../helpers/vuex_action_helper';
+import MockAdapter from 'axios-mock-adapter';
describe('Multi-file store actions', () => {
beforeEach(() => {
@@ -485,7 +488,7 @@ describe('Multi-file store actions', () => {
'path',
store.state,
[{ type: types.DELETE_ENTRY, payload: 'path' }],
- [{ type: 'burstUnusedSeal' }],
+ [{ type: 'burstUnusedSeal' }, { type: 'triggerFilesChange' }],
done,
);
});
@@ -499,15 +502,15 @@ describe('Multi-file store actions', () => {
testAction(
renameEntry,
- { path: 'test', name: 'new-name' },
+ { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
store.state,
[
{
type: types.RENAME_ENTRY,
- payload: { path: 'test', name: 'new-name', entryPath: null },
+ payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
},
],
- [{ type: 'deleteEntry', payload: 'test' }],
+ [{ type: 'deleteEntry', payload: 'test' }, { type: 'triggerFilesChange' }],
done,
);
});
@@ -527,21 +530,99 @@ describe('Multi-file store actions', () => {
testAction(
renameEntry,
- { path: 'test', name: 'new-name' },
+ { path: 'test', name: 'new-name', parentPath: 'parent-path' },
store.state,
[
{
type: types.RENAME_ENTRY,
- payload: { path: 'test', name: 'new-name', entryPath: null },
+ payload: { path: 'test', name: 'new-name', entryPath: null, parentPath: 'parent-path' },
},
],
[
- { type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-1' } },
- { type: 'renameEntry', payload: { path: 'test', name: 'new-name', entryPath: 'tree-2' } },
+ {
+ type: 'renameEntry',
+ payload: {
+ path: 'test',
+ name: 'new-name',
+ entryPath: 'tree-1',
+ parentPath: 'parent-path/new-name',
+ },
+ },
+ {
+ type: 'renameEntry',
+ payload: {
+ path: 'test',
+ name: 'new-name',
+ entryPath: 'tree-2',
+ parentPath: 'parent-path/new-name',
+ },
+ },
{ type: 'deleteEntry', payload: 'test' },
+ { type: 'triggerFilesChange' },
],
done,
);
});
});
+
+ describe('getBranchData', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('error', () => {
+ let dispatch;
+ const callParams = [
+ {
+ commit() {},
+ state: store.state,
+ },
+ {
+ projectId: 'abc/def',
+ branchId: 'master-testing',
+ },
+ ];
+
+ beforeEach(() => {
+ dispatch = jasmine.createSpy('dispatchSpy');
+ document.body.innerHTML += '<div class="flash-container"></div>';
+ });
+
+ afterEach(() => {
+ document.querySelector('.flash-container').remove();
+ });
+
+ it('passes the error further unchanged without dispatching any action when response is 404', done => {
+ mock.onGet(/(.*)/).replyOnce(404);
+
+ getBranchData(...callParams)
+ .then(done.fail)
+ .catch(e => {
+ expect(dispatch.calls.count()).toEqual(0);
+ expect(e.response.status).toEqual(404);
+ expect(document.querySelector('.flash-alert')).toBeNull();
+ done();
+ });
+ });
+
+ it('does not pass the error further and flashes an alert if error is not 404', done => {
+ mock.onGet(/(.*)/).replyOnce(418);
+
+ getBranchData(...callParams)
+ .then(done.fail)
+ .catch(e => {
+ expect(dispatch.calls.count()).toEqual(0);
+ expect(e.response).toBeUndefined();
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+ done();
+ });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index 9c135661997..735bbd47f55 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -180,6 +180,38 @@ describe('IDE store getters', () => {
});
});
+ describe('isOnDefaultBranch', () => {
+ it('returns false when no project exists', () => {
+ const localGetters = {
+ currentProject: undefined,
+ };
+
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy();
+ });
+
+ it("returns true when project's default branch matches current branch", () => {
+ const localGetters = {
+ currentProject: {
+ default_branch: 'master',
+ },
+ branchName: 'master',
+ };
+
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBeTruthy();
+ });
+
+ it("returns false when project's default branch doesn't match current branch", () => {
+ const localGetters = {
+ currentProject: {
+ default_branch: 'master',
+ },
+ branchName: 'feature',
+ };
+
+ expect(getters.isOnDefaultBranch({}, localGetters)).toBeFalsy();
+ });
+ });
+
describe('packageJson', () => {
it('returns package.json entry', () => {
localState.entries['package.json'] = { name: 'package.json' };
diff --git a/spec/javascripts/ide/stores/modules/commit/actions_spec.js b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
index 06b8b452319..5f7272311c8 100644
--- a/spec/javascripts/ide/stores/modules/commit/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/actions_spec.js
@@ -1,9 +1,13 @@
-import actions from '~/ide/stores/actions';
+import rootActions from '~/ide/stores/actions';
import store from '~/ide/stores';
import service from '~/ide/services';
import router from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
-import * as consts from '~/ide/stores/modules/commit/constants';
+import consts from '~/ide/stores/modules/commit/constants';
+import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
+import * as actions from '~/ide/stores/modules/commit/actions';
+import testAction from '../../../../helpers/vuex_action_helper';
+import { commitActionTypes } from '~/ide/constants';
import { resetStore, file } from 'spec/ide/helpers';
describe('IDE commit module actions', () => {
@@ -224,7 +228,7 @@ describe('IDE commit module actions', () => {
let visitUrl;
beforeEach(() => {
- visitUrl = spyOnDependency(actions, 'visitUrl');
+ visitUrl = spyOnDependency(rootActions, 'visitUrl');
document.body.innerHTML += '<div class="flash-container"></div>';
@@ -271,6 +275,7 @@ describe('IDE commit module actions', () => {
short_id: '123',
message: 'test message',
committed_date: 'date',
+ parent_ids: '321',
stats: {
additions: '1',
deletions: '2',
@@ -294,7 +299,7 @@ describe('IDE commit module actions', () => {
commit_message: 'testing 123',
actions: [
{
- action: 'update',
+ action: commitActionTypes.update,
file_path: jasmine.anything(),
content: undefined,
encoding: jasmine.anything(),
@@ -321,7 +326,7 @@ describe('IDE commit module actions', () => {
commit_message: 'testing 123',
actions: [
{
- action: 'update',
+ action: commitActionTypes.update,
file_path: jasmine.anything(),
content: undefined,
encoding: jasmine.anything(),
@@ -389,14 +394,15 @@ describe('IDE commit module actions', () => {
it('redirects to new merge request page', done => {
spyOn(eventHub, '$on');
- store.state.commit.commitAction = '3';
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.shouldCreateMR = true;
store
.dispatch('commit/commitChanges')
.then(() => {
expect(visitUrl).toHaveBeenCalledWith(
`webUrl/merge_requests/new?merge_request[source_branch]=${
- store.getters['commit/newBranchName']
+ store.getters['commit/placeholderBranchName']
}&merge_request[target_branch]=master`,
);
@@ -405,6 +411,21 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
+ it('does not redirect to new merge request page when shouldCreateMR is not checked', done => {
+ spyOn(eventHub, '$on');
+
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.shouldCreateMR = false;
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(visitUrl).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+
it('resets changed files before redirecting', done => {
spyOn(eventHub, '$on');
@@ -446,5 +467,213 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
});
+
+ describe('first commit of a branch', () => {
+ const COMMIT_RESPONSE = {
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ parent_ids: [],
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
+ };
+
+ it('commits TOGGLE_EMPTY_STATE mutation on empty repo', done => {
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: COMMIT_RESPONSE,
+ }),
+ );
+
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.commit.calls.allArgs()).toEqual(
+ jasmine.arrayContaining([
+ ['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)],
+ ]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not commmit TOGGLE_EMPTY_STATE mutation on existing project', done => {
+ COMMIT_RESPONSE.parent_ids.push('1234');
+ spyOn(service, 'commit').and.returnValue(
+ Promise.resolve({
+ data: COMMIT_RESPONSE,
+ }),
+ );
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/commitChanges')
+ .then(() => {
+ expect(store.commit.calls.allArgs()).not.toEqual(
+ jasmine.arrayContaining([
+ ['TOGGLE_EMPTY_STATE', jasmine.any(Object), jasmine.any(Object)],
+ ]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('toggleShouldCreateMR', () => {
+ it('commits both toggle and interacting with MR checkbox actions', done => {
+ testAction(
+ actions.toggleShouldCreateMR,
+ {},
+ store.state,
+ [
+ { type: mutationTypes.TOGGLE_SHOULD_CREATE_MR },
+ { type: mutationTypes.INTERACT_WITH_NEW_MR },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setShouldCreateMR', () => {
+ beforeEach(() => {
+ store.state.projects = {
+ project: {
+ default_branch: 'master',
+ branches: {
+ master: {
+ name: 'master',
+ },
+ feature: {
+ name: 'feature',
+ },
+ },
+ },
+ };
+
+ store.state.currentProjectId = 'project';
+ });
+
+ it('sets to false when the current branch already has an MR', done => {
+ store.state.commit.currentMergeRequestId = 1;
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.currentMergeRequestId = '1';
+ store.state.currentBranchId = 'feature';
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit.calls.allArgs()[0]).toEqual(
+ jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('changes to false when current branch is the default branch and user has not interacted', done => {
+ store.state.commit.interactedWithNewMR = false;
+ store.state.currentBranchId = 'master';
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit.calls.allArgs()[0]).toEqual(
+ jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('changes to true when "create new branch" is selected and user has not interacted', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.interactedWithNewMR = false;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit.calls.allArgs()[0]).toEqual(
+ jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, true]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not change anything if user has interacted and comitting to new branch', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.interactedWithNewMR = true;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not change anything if user has interacted and comitting to branch without MR', done => {
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.commit.currentMergeRequestId = null;
+ store.state.commit.interactedWithNewMR = true;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('still changes to false if hiding the checkbox', done => {
+ store.state.currentBranchId = 'feature';
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.currentMergeRequestId = '1';
+ store.state.commit.interactedWithNewMR = true;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit.calls.allArgs()[0]).toEqual(
+ jasmine.arrayContaining([`commit/${mutationTypes.TOGGLE_SHOULD_CREATE_MR}`, false]),
+ );
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('does not change to false when on master and user has interacted even if MR exists', done => {
+ store.state.currentBranchId = 'master';
+ store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.currentMergeRequestId = '1';
+ store.state.commit.interactedWithNewMR = true;
+ spyOn(store, 'commit').and.callThrough();
+
+ store
+ .dispatch('commit/setShouldCreateMR')
+ .then(() => {
+ expect(store.commit).not.toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/ide/stores/modules/commit/getters_spec.js b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
index 3f4bf407a1f..6e71a790deb 100644
--- a/spec/javascripts/ide/stores/modules/commit/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/commit/getters_spec.js
@@ -1,5 +1,5 @@
import commitState from '~/ide/stores/modules/commit/state';
-import * as consts from '~/ide/stores/modules/commit/constants';
+import consts from '~/ide/stores/modules/commit/constants';
import * as getters from '~/ide/stores/modules/commit/getters';
describe('IDE commit module getters', () => {
@@ -29,11 +29,11 @@ describe('IDE commit module getters', () => {
});
});
- describe('newBranchName', () => {
+ describe('placeholderBranchName', () => {
it('includes username, currentBranchId, patch & random number', () => {
gon.current_username = 'username';
- const branch = getters.newBranchName(state, null, {
+ const branch = getters.placeholderBranchName(state, null, {
currentBranchId: 'testing',
});
@@ -46,7 +46,7 @@ describe('IDE commit module getters', () => {
currentBranchId: 'master',
};
const localGetters = {
- newBranchName: 'newBranchName',
+ placeholderBranchName: 'placeholder-branch-name',
};
beforeEach(() => {
@@ -59,25 +59,28 @@ describe('IDE commit module getters', () => {
expect(getters.branchName(state, null, rootState)).toBe('master');
});
- ['COMMIT_TO_NEW_BRANCH', 'COMMIT_TO_NEW_BRANCH_MR'].forEach(type => {
- describe(type, () => {
- beforeEach(() => {
- Object.assign(state, {
- commitAction: consts[type],
- });
+ describe('COMMIT_TO_NEW_BRANCH', () => {
+ beforeEach(() => {
+ Object.assign(state, {
+ commitAction: consts.COMMIT_TO_NEW_BRANCH,
});
+ });
- it('uses newBranchName when not empty', () => {
- expect(getters.branchName(state, localGetters, rootState)).toBe('state-newBranchName');
+ it('uses newBranchName when not empty', () => {
+ const newBranchName = 'nonempty-branch-name';
+ Object.assign(state, {
+ newBranchName,
});
- it('uses getters newBranchName when state newBranchName is empty', () => {
- Object.assign(state, {
- newBranchName: '',
- });
+ expect(getters.branchName(state, localGetters, rootState)).toBe(newBranchName);
+ });
- expect(getters.branchName(state, localGetters, rootState)).toBe('newBranchName');
+ it('uses placeholderBranchName when state newBranchName is empty', () => {
+ Object.assign(state, {
+ newBranchName: '',
});
+
+ expect(getters.branchName(state, localGetters, rootState)).toBe('placeholder-branch-name');
});
});
});
diff --git a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
index 734233100ab..548962c7a92 100644
--- a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
@@ -69,18 +69,16 @@ describe('IDE file templates actions', () => {
describe('fetchTemplateTypes', () => {
describe('success', () => {
- let nextPage;
+ const pages = [[{ name: 'MIT' }], [{ name: 'Apache' }], [{ name: 'CC' }]];
beforeEach(() => {
- mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(() => [
- 200,
- [
- {
- name: 'MIT',
- },
- ],
- { 'X-NEXT-PAGE': nextPage },
- ]);
+ mock.onGet(/api\/(.*)\/templates\/licenses/).reply(({ params }) => {
+ const pageNum = params.page;
+ const page = pages[pageNum - 1];
+ const hasNextPage = pageNum < pages.length;
+
+ return [200, page, hasNextPage ? { 'X-NEXT-PAGE': pageNum + 1 } : {}];
+ });
});
it('rejects if selectedTemplateType is empty', done => {
@@ -112,43 +110,15 @@ describe('IDE file templates actions', () => {
},
{
type: 'receiveTemplateTypesSuccess',
- payload: [
- {
- name: 'MIT',
- },
- ],
- },
- ],
- done,
- );
- });
-
- it('dispatches actions for next page', done => {
- nextPage = '2';
- state.selectedTemplateType = {
- key: 'licenses',
- };
-
- testAction(
- actions.fetchTemplateTypes,
- null,
- state,
- [],
- [
- {
- type: 'requestTemplateTypes',
+ payload: pages[0],
},
{
type: 'receiveTemplateTypesSuccess',
- payload: [
- {
- name: 'MIT',
- },
- ],
+ payload: pages[0].concat(pages[1]),
},
{
- type: 'fetchTemplateTypes',
- payload: 2,
+ type: 'receiveTemplateTypesSuccess',
+ payload: pages[0].concat(pages[1]).concat(pages[2]),
},
],
done,
diff --git a/spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js b/spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js
deleted file mode 100644
index 8e0e3ae99a1..00000000000
--- a/spec/javascripts/ide/stores/modules/file_templates/mutations_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import createState from '~/ide/stores/modules/file_templates/state';
-import * as types from '~/ide/stores/modules/file_templates/mutation_types';
-import mutations from '~/ide/stores/modules/file_templates/mutations';
-
-describe('IDE file templates mutations', () => {
- let state;
-
- beforeEach(() => {
- state = createState();
- });
-
- describe(types.REQUEST_TEMPLATE_TYPES, () => {
- it('sets isLoading', () => {
- mutations[types.REQUEST_TEMPLATE_TYPES](state);
-
- expect(state.isLoading).toBe(true);
- });
- });
-
- describe(types.RECEIVE_TEMPLATE_TYPES_ERROR, () => {
- it('sets isLoading', () => {
- state.isLoading = true;
-
- mutations[types.RECEIVE_TEMPLATE_TYPES_ERROR](state);
-
- expect(state.isLoading).toBe(false);
- });
- });
-
- describe(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, () => {
- it('sets isLoading to false', () => {
- state.isLoading = true;
-
- mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, []);
-
- expect(state.isLoading).toBe(false);
- });
-
- it('sets templates', () => {
- mutations[types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, ['test']);
-
- expect(state.templates).toEqual(['test']);
- });
- });
-
- describe(types.SET_SELECTED_TEMPLATE_TYPE, () => {
- it('sets selectedTemplateType', () => {
- mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, 'type');
-
- expect(state.selectedTemplateType).toBe('type');
- });
-
- it('clears templates', () => {
- state.templates = ['test'];
-
- mutations[types.SET_SELECTED_TEMPLATE_TYPE](state, 'type');
-
- expect(state.templates).toEqual([]);
- });
- });
-
- describe(types.SET_UPDATE_SUCCESS, () => {
- it('sets updateSuccess', () => {
- mutations[types.SET_UPDATE_SUCCESS](state, true);
-
- expect(state.updateSuccess).toBe(true);
- });
- });
-});
diff --git a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
index eb7346bd5fc..b558c45f574 100644
--- a/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
+++ b/spec/javascripts/ide/stores/modules/pipelines/mutations_spec.js
@@ -27,63 +27,71 @@ describe('IDE pipelines mutations', () => {
});
describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
- it('sets loading to false on success', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
- mockedState,
- fullPipelinesResponse.data.pipelines[0],
- );
+ const itSetsPipelineLoadingStates = () => {
+ it('sets has loaded to true', () => {
+ expect(mockedState.hasLoadedPipeline).toBe(true);
+ });
- expect(mockedState.isLoadingPipeline).toBe(false);
- });
+ it('sets loading to false on success', () => {
+ expect(mockedState.isLoadingPipeline).toBe(false);
+ });
+ };
+
+ describe('with pipeline', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
+ mockedState,
+ fullPipelinesResponse.data.pipelines[0],
+ );
+ });
- it('sets latestPipeline', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
- mockedState,
- fullPipelinesResponse.data.pipelines[0],
- );
+ itSetsPipelineLoadingStates();
- expect(mockedState.latestPipeline).toEqual({
- id: '51',
- path: 'test',
- commit: { id: '123' },
- details: { status: jasmine.any(Object) },
- yamlError: undefined,
+ it('sets latestPipeline', () => {
+ expect(mockedState.latestPipeline).toEqual({
+ id: '51',
+ path: 'test',
+ commit: { id: '123' },
+ details: { status: jasmine.any(Object) },
+ yamlError: undefined,
+ });
});
- });
- it('does not set latest pipeline if pipeline is null', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
-
- expect(mockedState.latestPipeline).toEqual(false);
+ it('sets stages', () => {
+ expect(mockedState.stages.length).toBe(2);
+ expect(mockedState.stages).toEqual([
+ {
+ id: 0,
+ dropdownPath: stages[0].dropdown_path,
+ name: stages[0].name,
+ status: stages[0].status,
+ isCollapsed: false,
+ isLoading: false,
+ jobs: [],
+ },
+ {
+ id: 1,
+ dropdownPath: stages[1].dropdown_path,
+ name: stages[1].name,
+ status: stages[1].status,
+ isCollapsed: false,
+ isLoading: false,
+ jobs: [],
+ },
+ ]);
+ });
});
- it('sets stages', () => {
- mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
- mockedState,
- fullPipelinesResponse.data.pipelines[0],
- );
+ describe('with null', () => {
+ beforeEach(() => {
+ mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
+ });
- expect(mockedState.stages.length).toBe(2);
- expect(mockedState.stages).toEqual([
- {
- id: 0,
- dropdownPath: stages[0].dropdown_path,
- name: stages[0].name,
- status: stages[0].status,
- isCollapsed: false,
- isLoading: false,
- jobs: [],
- },
- {
- id: 1,
- dropdownPath: stages[1].dropdown_path,
- name: stages[1].name,
- status: stages[1].status,
- isCollapsed: false,
- isLoading: false,
- jobs: [],
- },
- ]);
+ itSetsPipelineLoadingStates();
+
+ it('does not set latest pipeline if pipeline is null', () => {
+ expect(mockedState.latestPipeline).toEqual(null);
+ });
});
});
diff --git a/spec/javascripts/ide/stores/mutations/branch_spec.js b/spec/javascripts/ide/stores/mutations/branch_spec.js
deleted file mode 100644
index 29eb859ddaf..00000000000
--- a/spec/javascripts/ide/stores/mutations/branch_spec.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import mutations from '~/ide/stores/mutations/branch';
-import state from '~/ide/stores/state';
-
-describe('Multi-file store branch mutations', () => {
- let localState;
-
- beforeEach(() => {
- localState = state();
- });
-
- describe('SET_CURRENT_BRANCH', () => {
- it('sets currentBranch', () => {
- mutations.SET_CURRENT_BRANCH(localState, 'master');
-
- expect(localState.currentBranchId).toBe('master');
- });
- });
-
- describe('SET_BRANCH_COMMIT', () => {
- it('sets the last commit on current project', () => {
- localState.projects = {
- Example: {
- branches: {
- master: {},
- },
- },
- };
-
- mutations.SET_BRANCH_COMMIT(localState, {
- projectId: 'Example',
- branchId: 'master',
- commit: {
- title: 'Example commit',
- },
- });
-
- expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
- });
- });
-});
diff --git a/spec/javascripts/ide/stores/mutations/tree_spec.js b/spec/javascripts/ide/stores/mutations/tree_spec.js
index 67e9f7509da..7f9c978aa46 100644
--- a/spec/javascripts/ide/stores/mutations/tree_spec.js
+++ b/spec/javascripts/ide/stores/mutations/tree_spec.js
@@ -26,17 +26,11 @@ describe('Multi-file store tree mutations', () => {
});
describe('SET_DIRECTORY_DATA', () => {
- const data = [
- {
- name: 'tree',
- },
- {
- name: 'submodule',
- },
- {
- name: 'blob',
- },
- ];
+ let data;
+
+ beforeEach(() => {
+ data = [file('tree'), file('foo'), file('blob')];
+ });
it('adds directory data', () => {
localState.trees['project/master'] = {
@@ -52,7 +46,7 @@ describe('Multi-file store tree mutations', () => {
expect(tree.tree.length).toBe(3);
expect(tree.tree[0].name).toBe('tree');
- expect(tree.tree[1].name).toBe('submodule');
+ expect(tree.tree[1].name).toBe('foo');
expect(tree.tree[2].name).toBe('blob');
});
@@ -65,6 +59,49 @@ describe('Multi-file store tree mutations', () => {
expect(localState.trees['project/master'].loading).toBe(true);
});
+
+ it('does not override tree already in state, but merges the two with correct order', () => {
+ const openedFile = file('new');
+
+ localState.trees['project/master'] = {
+ loading: true,
+ tree: [openedFile],
+ };
+
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ const { tree } = localState.trees['project/master'];
+
+ expect(tree.length).toBe(4);
+ expect(tree[0].name).toBe('blob');
+ expect(tree[1].name).toBe('foo');
+ expect(tree[2].name).toBe('new');
+ expect(tree[3].name).toBe('tree');
+ });
+
+ it('returns tree unchanged if the opened file is already in the tree', () => {
+ const openedFile = file('foo');
+ localState.trees['project/master'] = {
+ loading: true,
+ tree: [openedFile],
+ };
+
+ mutations.SET_DIRECTORY_DATA(localState, {
+ data,
+ treePath: 'project/master',
+ });
+
+ const { tree } = localState.trees['project/master'];
+
+ expect(tree.length).toBe(3);
+
+ expect(tree[0].name).toBe('tree');
+ expect(tree[1].name).toBe('foo');
+ expect(tree[2].name).toBe('blob');
+ });
});
describe('REMOVE_ALL_CHANGES_FILES', () => {
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 41dd3d3c67f..5ee098bf17f 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -298,7 +298,12 @@ describe('Multi-file store mutations', () => {
});
it('creates new renamed entry', () => {
- mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+ mutations.RENAME_ENTRY(localState, {
+ path: 'oldPath',
+ name: 'newPath',
+ entryPath: null,
+ parentPath: '',
+ });
expect(localState.entries.newPath).toEqual({
...localState.entries.oldPath,
@@ -335,7 +340,12 @@ describe('Multi-file store mutations', () => {
...file(),
};
- mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+ mutations.RENAME_ENTRY(localState, {
+ path: 'oldPath',
+ name: 'newPath',
+ entryPath: null,
+ parentPath: 'parentPath',
+ });
expect(localState.entries.parentPath.tree.length).toBe(1);
});
diff --git a/spec/javascripts/ide/stores/utils_spec.js b/spec/javascripts/ide/stores/utils_spec.js
index 9f18034f8a3..debe1c4acee 100644
--- a/spec/javascripts/ide/stores/utils_spec.js
+++ b/spec/javascripts/ide/stores/utils_spec.js
@@ -1,4 +1,5 @@
import * as utils from '~/ide/stores/utils';
+import { commitActionTypes } from '~/ide/constants';
import { file } from '../helpers';
describe('Multi-file store utils', () => {
@@ -107,7 +108,7 @@ describe('Multi-file store utils', () => {
commit_message: 'commit message',
actions: [
{
- action: 'update',
+ action: commitActionTypes.update,
file_path: 'staged',
content: 'updated file content',
encoding: 'text',
@@ -115,7 +116,7 @@ describe('Multi-file store utils', () => {
previous_path: undefined,
},
{
- action: 'create',
+ action: commitActionTypes.create,
file_path: 'added',
content: 'new file content',
encoding: 'base64',
@@ -123,7 +124,7 @@ describe('Multi-file store utils', () => {
previous_path: undefined,
},
{
- action: 'delete',
+ action: commitActionTypes.delete,
file_path: 'deletedFile',
content: undefined,
encoding: 'text',
@@ -170,7 +171,7 @@ describe('Multi-file store utils', () => {
commit_message: 'prebuilt test commit message',
actions: [
{
- action: 'update',
+ action: commitActionTypes.update,
file_path: 'staged',
content: 'updated file content',
encoding: 'text',
@@ -178,7 +179,7 @@ describe('Multi-file store utils', () => {
previous_path: undefined,
},
{
- action: 'create',
+ action: commitActionTypes.create,
file_path: 'added',
content: 'new file content',
encoding: 'base64',
@@ -193,19 +194,19 @@ describe('Multi-file store utils', () => {
describe('commitActionForFile', () => {
it('returns deleted for deleted file', () => {
- expect(utils.commitActionForFile({ deleted: true })).toBe('delete');
+ expect(utils.commitActionForFile({ deleted: true })).toBe(commitActionTypes.delete);
});
it('returns create for tempFile', () => {
- expect(utils.commitActionForFile({ tempFile: true })).toBe('create');
+ expect(utils.commitActionForFile({ tempFile: true })).toBe(commitActionTypes.create);
});
it('returns move for moved file', () => {
- expect(utils.commitActionForFile({ prevPath: 'test' })).toBe('move');
+ expect(utils.commitActionForFile({ prevPath: 'test' })).toBe(commitActionTypes.move);
});
it('returns update by default', () => {
- expect(utils.commitActionForFile({})).toBe('update');
+ expect(utils.commitActionForFile({})).toBe(commitActionTypes.update);
});
});
@@ -235,4 +236,129 @@ describe('Multi-file store utils', () => {
]);
});
});
+
+ describe('mergeTrees', () => {
+ let fromTree;
+ let toTree;
+
+ beforeEach(() => {
+ fromTree = [file('foo')];
+ toTree = [file('bar')];
+ });
+
+ it('merges simple trees with sorting the result', () => {
+ toTree = [file('beta'), file('alpha'), file('gamma')];
+ const res = utils.mergeTrees(fromTree, toTree);
+
+ expect(res.length).toEqual(4);
+ expect(res[0].name).toEqual('alpha');
+ expect(res[1].name).toEqual('beta');
+ expect(res[2].name).toEqual('foo');
+ expect(res[3].name).toEqual('gamma');
+ expect(res[2]).toBe(fromTree[0]);
+ });
+
+ it('handles edge cases', () => {
+ expect(utils.mergeTrees({}, []).length).toEqual(0);
+
+ let res = utils.mergeTrees({}, toTree);
+
+ expect(res.length).toEqual(1);
+ expect(res[0].name).toEqual('bar');
+
+ res = utils.mergeTrees(fromTree, []);
+
+ expect(res.length).toEqual(1);
+ expect(res[0].name).toEqual('foo');
+ expect(res[0]).toBe(fromTree[0]);
+ });
+
+ it('merges simple trees without producing duplicates', () => {
+ toTree.push(file('foo'));
+
+ const res = utils.mergeTrees(fromTree, toTree);
+
+ expect(res.length).toEqual(2);
+ expect(res[0].name).toEqual('bar');
+ expect(res[1].name).toEqual('foo');
+ expect(res[1]).not.toBe(fromTree[0]);
+ });
+
+ it('merges nested tree into the main one without duplicates', () => {
+ fromTree[0].tree.push({
+ ...file('alpha'),
+ path: 'foo/alpha',
+ tree: [
+ {
+ ...file('beta.md'),
+ path: 'foo/alpha/beta.md',
+ },
+ ],
+ });
+
+ toTree.push({
+ ...file('foo'),
+ tree: [
+ {
+ ...file('alpha'),
+ path: 'foo/alpha',
+ tree: [
+ {
+ ...file('gamma.md'),
+ path: 'foo/alpha/gamma.md',
+ },
+ ],
+ },
+ ],
+ });
+
+ const res = utils.mergeTrees(fromTree, toTree);
+
+ expect(res.length).toEqual(2);
+ expect(res[1].name).toEqual('foo');
+
+ const finalBranch = res[1].tree[0].tree;
+
+ expect(finalBranch.length).toEqual(2);
+ expect(finalBranch[0].name).toEqual('beta.md');
+ expect(finalBranch[1].name).toEqual('gamma.md');
+ });
+
+ it('marks correct folders as opened as the parsing goes on', () => {
+ fromTree[0].tree.push({
+ ...file('alpha'),
+ path: 'foo/alpha',
+ tree: [
+ {
+ ...file('beta.md'),
+ path: 'foo/alpha/beta.md',
+ },
+ ],
+ });
+
+ toTree.push({
+ ...file('foo'),
+ tree: [
+ {
+ ...file('alpha'),
+ path: 'foo/alpha',
+ tree: [
+ {
+ ...file('gamma.md'),
+ path: 'foo/alpha/gamma.md',
+ },
+ ],
+ },
+ ],
+ });
+
+ const res = utils.mergeTrees(fromTree, toTree);
+
+ expect(res[1].name).toEqual('foo');
+ expect(res[1].opened).toEqual(true);
+
+ expect(res[1].tree[0].name).toEqual('alpha');
+ expect(res[1].tree[0].opened).toEqual(true);
+ });
+ });
});
diff --git a/spec/javascripts/import_projects/components/import_projects_table_spec.js b/spec/javascripts/import_projects/components/import_projects_table_spec.js
deleted file mode 100644
index a1ff84ce259..00000000000
--- a/spec/javascripts/import_projects/components/import_projects_table_spec.js
+++ /dev/null
@@ -1,186 +0,0 @@
-import Vue from 'vue';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import store from '~/import_projects/store';
-import importProjectsTable from '~/import_projects/components/import_projects_table.vue';
-import STATUS_MAP from '~/import_projects/constants';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
-
-describe('ImportProjectsTable', () => {
- let vm;
- let mock;
- const reposPath = '/repos-path';
- const jobsPath = '/jobs-path';
- const providerTitle = 'THE PROVIDER';
- const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
- const importedProject = {
- id: 1,
- fullPath: 'fullPath',
- importStatus: 'started',
- providerLink: 'providerLink',
- importSource: 'importSource',
- };
-
- function createComponent() {
- const ImportProjectsTable = Vue.extend(importProjectsTable);
-
- const component = new ImportProjectsTable({
- store,
- propsData: {
- providerTitle,
- },
- }).$mount();
-
- component.$store.dispatch('stopJobsPolling');
-
- return component;
- }
-
- beforeEach(() => {
- store.dispatch('setInitialData', { reposPath });
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- vm.$destroy();
- mock.restore();
- });
-
- it('renders a loading icon whilst repos are loading', done => {
- mock.restore(); // Stop the mock adapter from responding to the request, keeping the spinner up
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders a table with imported projects and provider repos', done => {
- const response = {
- importedProjects: [importedProject],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).not.toBeNull();
- expect(vm.$el.querySelector('.import-jobs-from-col').innerText).toMatch(
- `From ${providerTitle}`,
- );
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('renders an empty state if there are no imported projects or provider repos', done => {
- const response = {
- importedProjects: [],
- providerRepos: [],
- namespaces: [],
- };
- mock.onGet(reposPath).reply(200, response);
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-loading-button-icon')).toBeNull();
- expect(vm.$el.querySelector('.table')).toBeNull();
- expect(vm.$el.innerText).toMatch(`No ${providerTitle} repositories available to import`);
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('imports provider repos if bulk import button is clicked', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [],
- providerRepos: [providerRepo],
- namespaces: [{ path: 'path' }],
- };
-
- mock.onGet(reposPath).replyOnce(200, response);
- mock.onPost(importPath).replyOnce(200, importedProject);
-
- store.dispatch('setInitialData', { importPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).not.toBeNull();
-
- vm.$el.querySelector('.js-import-all').click();
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector('.js-provider-repo')).toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-
- it('polls to update the status of imported projects', done => {
- const importPath = '/import-path';
- const response = {
- importedProjects: [importedProject],
- providerRepos: [],
- namespaces: [{ path: 'path' }],
- };
- const updatedProjects = [
- {
- id: importedProject.id,
- importStatus: 'finished',
- },
- ];
-
- mock.onGet(reposPath).replyOnce(200, response);
-
- store.dispatch('setInitialData', { importPath, jobsPath });
-
- vm = createComponent();
-
- setTimeoutPromise()
- .then(() => {
- const statusObject = STATUS_MAP[importedProject.importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
-
- mock.onGet(jobsPath).replyOnce(200, updatedProjects);
- return vm.$store.dispatch('restartJobsPolling');
- })
- .then(() => setTimeoutPromise())
- .then(() => {
- const statusObject = STATUS_MAP[updatedProjects[0].importStatus];
-
- expect(vm.$el.querySelector('.js-imported-project')).not.toBeNull();
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- })
- .then(() => done())
- .catch(() => done.fail());
- });
-});
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
index 4f4c9a7b463..069e2cb07b5 100644
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
describe('IntegrationSettingsForm', () => {
- const FIXTURE = 'services/edit_service.html.raw';
+ const FIXTURE = 'services/edit_service.html';
preloadFixtures(FIXTURE);
beforeEach(() => {
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 0ccf771c7ef..2770743937e 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -75,7 +75,7 @@ describe('Issuable output', () => {
.then(() => {
expect(document.querySelector('title').innerText).toContain('this is a title (#1)');
expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>this is a title</p>');
- expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>this is a description!</p>');
+ expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>this is a description!</p>');
expect(vm.$el.querySelector('.js-task-list-field').value).toContain(
'this is a description',
);
@@ -92,7 +92,7 @@ describe('Issuable output', () => {
.then(() => {
expect(document.querySelector('title').innerText).toContain('2 (#1)');
expect(vm.$el.querySelector('.title').innerHTML).toContain('<p>2</p>');
- expect(vm.$el.querySelector('.wiki').innerHTML).toContain('<p>42</p>');
+ expect(vm.$el.querySelector('.md').innerHTML).toContain('<p>42</p>');
expect(vm.$el.querySelector('.js-task-list-field').value).toContain('42');
expect(vm.$el.querySelector('.edited-text')).toBeTruthy();
expect(formatText(vm.$el.querySelector('.edited-text').innerText)).toMatch(
@@ -470,4 +470,51 @@ describe('Issuable output', () => {
.catch(done.fail);
});
});
+
+ describe('issueChanged', () => {
+ beforeEach(() => {
+ vm.store.formState.title = '';
+ vm.store.formState.description = '';
+ vm.initialDescriptionText = '';
+ vm.initialTitleText = '';
+ });
+
+ it('returns true when title is changed', () => {
+ vm.store.formState.title = 'RandomText';
+
+ expect(vm.issueChanged).toBe(true);
+ });
+
+ it('returns false when title is empty null', () => {
+ vm.store.formState.title = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns false when `initialTitleText` is null and `formState.title` is empty string', () => {
+ vm.store.formState.title = '';
+ vm.initialTitleText = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns true when description is changed', () => {
+ vm.store.formState.description = 'RandomText';
+
+ expect(vm.issueChanged).toBe(true);
+ });
+
+ it('returns false when description is empty null', () => {
+ vm.store.formState.title = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+
+ it('returns false when `initialDescriptionText` is null and `formState.description` is empty string', () => {
+ vm.store.formState.description = '';
+ vm.initialDescriptionText = null;
+
+ expect(vm.issueChanged).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 2eeed6770be..7e00fbf2745 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -43,12 +43,12 @@ describe('Description component', () => {
Vue.nextTick(() => {
expect(
- vm.$el.querySelector('.wiki').classList.contains('issue-realtime-pre-pulse'),
+ vm.$el.querySelector('.md').classList.contains('issue-realtime-pre-pulse'),
).toBeTruthy();
setTimeout(() => {
expect(
- vm.$el.querySelector('.wiki').classList.contains('issue-realtime-trigger-pulse'),
+ vm.$el.querySelector('.md').classList.contains('issue-realtime-trigger-pulse'),
).toBeTruthy();
done();
diff --git a/spec/javascripts/issue_show/components/fields/description_spec.js b/spec/javascripts/issue_show/components/fields/description_spec.js
index 2c3efc8d4d4..f5f87a6bfbf 100644
--- a/spec/javascripts/issue_show/components/fields/description_spec.js
+++ b/spec/javascripts/issue_show/components/fields/description_spec.js
@@ -63,4 +63,8 @@ describe('Description field component', () => {
expect(eventHub.$emit).toHaveBeenCalled();
});
+
+ it('has a ref named `textarea`', () => {
+ expect(vm.$refs.textarea).not.toBeNull();
+ });
});
diff --git a/spec/javascripts/issue_show/components/fields/title_spec.js b/spec/javascripts/issue_show/components/fields/title_spec.js
index 4b96a1feb29..62dff983250 100644
--- a/spec/javascripts/issue_show/components/fields/title_spec.js
+++ b/spec/javascripts/issue_show/components/fields/title_spec.js
@@ -41,4 +41,8 @@ describe('Title field component', () => {
expect(eventHub.$emit).toHaveBeenCalled();
});
+
+ it('has a ref named `input`', () => {
+ expect(vm.$refs.input).not.toBeNull();
+ });
});
diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js
index 523954013cf..b0f4ab2b12d 100644
--- a/spec/javascripts/issue_show/components/form_spec.js
+++ b/spec/javascripts/issue_show/components/form_spec.js
@@ -1,10 +1,17 @@
import Vue from 'vue';
import formComponent from '~/issue_show/components/form.vue';
+import eventHub from '~/issue_show/event_hub';
describe('Inline edit form component', () => {
let vm;
+ let autosave;
+ let autosaveObj;
beforeEach(done => {
+ autosaveObj = { reset: jasmine.createSpy() };
+
+ autosave = spyOnDependency(formComponent, 'Autosave').and.returnValue(autosaveObj);
+
const Component = Vue.extend(formComponent);
vm = new Component({
@@ -53,4 +60,22 @@ describe('Inline edit form component', () => {
done();
});
});
+
+ it('initialized Autosave on mount', () => {
+ expect(autosave).toHaveBeenCalledTimes(2);
+ });
+
+ it('calls reset on autosave when eventHub emits appropriate events', () => {
+ eventHub.$emit('close.form');
+
+ expect(autosaveObj.reset).toHaveBeenCalledTimes(2);
+
+ eventHub.$emit('delete.issuable');
+
+ expect(autosaveObj.reset).toHaveBeenCalledTimes(4);
+
+ eventHub.$emit('update.issuable');
+
+ expect(autosaveObj.reset).toHaveBeenCalledTimes(6);
+ });
});
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 7be495d1d35..966aee72abb 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -9,9 +9,9 @@ import '~/lib/utils/text_utility';
describe('Issue', function() {
let $boxClosed, $boxOpen, $btn;
- preloadFixtures('issues/closed-issue.html.raw');
- preloadFixtures('issues/issue-with-task-list.html.raw');
- preloadFixtures('issues/open-issue.html.raw');
+ preloadFixtures('issues/closed-issue.html');
+ preloadFixtures('issues/issue-with-task-list.html');
+ preloadFixtures('issues/open-issue.html');
function expectErrorMessage() {
const $flashMessage = $('div.flash-alert');
@@ -105,15 +105,14 @@ describe('Issue', function() {
beforeEach(function() {
if (isIssueInitiallyOpen) {
- loadFixtures('issues/open-issue.html.raw');
+ loadFixtures('issues/open-issue.html');
} else {
- loadFixtures('issues/closed-issue.html.raw');
+ loadFixtures('issues/closed-issue.html');
}
mock = new MockAdapter(axios);
mock.onGet(/(.*)\/related_branches$/).reply(200, {});
- mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {});
findElements(isIssueInitiallyOpen);
this.issue = new Issue();
diff --git a/spec/javascripts/jobs/components/artifacts_block_spec.js b/spec/javascripts/jobs/components/artifacts_block_spec.js
index 27d480ef2ea..58998d038e5 100644
--- a/spec/javascripts/jobs/components/artifacts_block_spec.js
+++ b/spec/javascripts/jobs/components/artifacts_block_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import component from '~/jobs/components/artifacts_block.vue';
import mountComponent from '../../helpers/vue_mount_component_helper';
-import { trimText } from '../../helpers/vue_component_helper';
+import { trimText } from '../../helpers/text_helper';
describe('Artifacts block', () => {
const Component = Vue.extend(component);
diff --git a/spec/javascripts/jobs/components/commit_block_spec.js b/spec/javascripts/jobs/components/commit_block_spec.js
index 98eba3ac976..c02f564d01a 100644
--- a/spec/javascripts/jobs/components/commit_block_spec.js
+++ b/spec/javascripts/jobs/components/commit_block_spec.js
@@ -9,6 +9,7 @@ describe('Commit block', () => {
const props = {
commit: {
short_id: '1f0fb84f',
+ id: '1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
title: 'Update README.md',
},
@@ -42,7 +43,7 @@ describe('Commit block', () => {
it('renders clipboard button', () => {
expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(
- props.commit.short_id,
+ props.commit.id,
);
});
});
diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js
index ba5d672f189..f28d2c2a882 100644
--- a/spec/javascripts/jobs/components/job_app_spec.js
+++ b/spec/javascripts/jobs/components/job_app_spec.js
@@ -17,6 +17,7 @@ describe('Job App ', () => {
const props = {
endpoint: `${gl.TEST_HOST}jobs/123.json`,
runnerHelpUrl: 'help/runner',
+ deploymentHelpUrl: 'help/deployment',
runnerSettingsUrl: 'settings/ci-cd/runners',
terminalPath: 'jobs/123/terminal',
pagePath: `${gl.TEST_HOST}jobs/123`,
@@ -89,9 +90,12 @@ describe('Job App ', () => {
describe('triggered job', () => {
beforeEach(() => {
+ const aYearAgo = new Date();
+ aYearAgo.setFullYear(aYearAgo.getFullYear() - 1);
+
mock
.onGet(props.endpoint)
- .replyOnce(200, Object.assign({}, job, { started: '2017-05-24T10:59:52.000+01:00' }));
+ .replyOnce(200, Object.assign({}, job, { started: aYearAgo.toISOString() }));
vm = mountComponentWithStore(Component, { props, store });
});
@@ -253,6 +257,41 @@ describe('Job App ', () => {
});
});
+ describe('unmet prerequisites block', () => {
+ it('renders unmet prerequisites block when there is an unmet prerequisites failure', done => {
+ mock.onGet(props.endpoint).replyOnce(
+ 200,
+ Object.assign({}, job, {
+ status: {
+ group: 'failed',
+ icon: 'status_failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ illustration: {
+ content: 'Retry this job in order to create the necessary resources.',
+ image: 'path',
+ size: 'svg-430',
+ title: 'Failed to create resources',
+ },
+ },
+ failure_reason: 'unmet_prerequisites',
+ has_trace: false,
+ runners: {
+ available: true,
+ },
+ tags: [],
+ }),
+ );
+ vm = mountComponentWithStore(Component, { props, store });
+
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.js-job-failed')).not.toBeNull();
+ done();
+ }, 0);
+ });
+ });
+
describe('environments block', () => {
it('renders environment block when job has environment', done => {
mock.onGet(props.endpoint).replyOnce(
diff --git a/spec/javascripts/jobs/components/sidebar_spec.js b/spec/javascripts/jobs/components/sidebar_spec.js
index 3a02351460c..740bc3d0491 100644
--- a/spec/javascripts/jobs/components/sidebar_spec.js
+++ b/spec/javascripts/jobs/components/sidebar_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import sidebarDetailsBlock from '~/jobs/components/sidebar.vue';
import createStore from '~/jobs/store';
-import job, { stages, jobsInStage } from '../mock_data';
+import job, { jobsInStage } from '../mock_data';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { trimText } from '../../helpers/vue_component_helper';
+import { trimText } from '../../helpers/text_helper';
describe('Sidebar details block', () => {
const SidebarComponent = Vue.extend(sidebarDetailsBlock);
@@ -131,18 +131,8 @@ describe('Sidebar details block', () => {
store.dispatch('receiveJobSuccess', job);
});
- describe('while fetching stages', () => {
- it('it does not render dropdown', () => {
- store.dispatch('requestStages');
- vm = mountComponentWithStore(SidebarComponent, { store });
-
- expect(vm.$el.querySelector('.js-selected-stage')).toBeNull();
- });
- });
-
describe('with stages', () => {
beforeEach(() => {
- store.dispatch('receiveStagesSuccess', stages);
vm = mountComponentWithStore(SidebarComponent, { store });
});
@@ -156,7 +146,6 @@ describe('Sidebar details block', () => {
describe('without jobs for stages', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
- store.dispatch('receiveStagesSuccess', stages);
vm = mountComponentWithStore(SidebarComponent, { store });
});
@@ -168,7 +157,6 @@ describe('Sidebar details block', () => {
describe('with jobs for stages', () => {
beforeEach(() => {
store.dispatch('receiveJobSuccess', job);
- store.dispatch('receiveStagesSuccess', stages);
store.dispatch('receiveJobsForStageSuccess', jobsInStage.latest_statuses);
vm = mountComponentWithStore(SidebarComponent, { store });
});
diff --git a/spec/javascripts/jobs/components/stages_dropdown_spec.js b/spec/javascripts/jobs/components/stages_dropdown_spec.js
index 9c731ae2f68..e98639bf21e 100644
--- a/spec/javascripts/jobs/components/stages_dropdown_spec.js
+++ b/spec/javascripts/jobs/components/stages_dropdown_spec.js
@@ -1,59 +1,168 @@
import Vue from 'vue';
import component from '~/jobs/components/stages_dropdown.vue';
+import { trimText } from 'spec/helpers/text_helper';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Stages Dropdown', () => {
const Component = Vue.extend(component);
let vm;
- beforeEach(() => {
- vm = mountComponent(Component, {
- pipeline: {
- id: 28029444,
- details: {
- status: {
- details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
- group: 'success',
- has_details: true,
- icon: 'status_success',
- label: 'passed',
- text: 'passed',
- tooltip: 'passed',
- },
- },
- path: 'pipeline/28029444',
+ const mockPipelineData = {
+ id: 28029444,
+ iid: 123,
+ details: {
+ status: {
+ details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
+ group: 'success',
+ has_details: true,
+ icon: 'status_success',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
},
- stages: [
- {
- name: 'build',
- },
- {
- name: 'test',
- },
- ],
- selectedStage: 'deploy',
+ },
+ path: 'pipeline/28029444',
+ flags: {
+ merge_request_pipeline: true,
+ detached_merge_request_pipeline: false,
+ },
+ merge_request: {
+ iid: 1234,
+ path: '/root/detached-merge-request-pipelines/merge_requests/1',
+ title: 'Update README.md',
+ source_branch: 'feature-1234',
+ source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1234',
+ target_branch: 'master',
+ target_branch_path: '/root/detached-merge-request-pipelines/branches/master',
+ },
+ ref: {
+ name: 'test-branch',
+ },
+ };
+
+ describe('without a merge request pipeline', () => {
+ let pipeline;
+
+ beforeEach(() => {
+ pipeline = JSON.parse(JSON.stringify(mockPipelineData));
+ delete pipeline.merge_request;
+ delete pipeline.flags.merge_request_pipeline;
+ delete pipeline.flags.detached_merge_request_pipeline;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ stages: [{ name: 'build' }, { name: 'test' }],
+ selectedStage: 'deploy',
+ });
});
- });
- afterEach(() => {
- vm.$destroy();
- });
+ afterEach(() => {
+ vm.$destroy();
+ });
- it('renders pipeline status', () => {
- expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull();
- });
+ it('renders pipeline status', () => {
+ expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull();
+ });
+
+ it('renders pipeline link', () => {
+ expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual(
+ 'pipeline/28029444',
+ );
+ });
+
+ it('renders dropdown with stages', () => {
+ expect(vm.$el.querySelector('.dropdown .js-stage-item').textContent).toContain('build');
+ });
+
+ it('rendes selected stage', () => {
+ expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy');
+ });
- it('renders pipeline link', () => {
- expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual(
- 'pipeline/28029444',
- );
+ it(`renders the pipeline info text like "Pipeline #123 (#12) for source_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for ${pipeline.ref.name}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
+
+ expect(actual).toBe(expected);
+ });
});
- it('renders dropdown with stages', () => {
- expect(vm.$el.querySelector('.dropdown .js-stage-item').textContent).toContain('build');
+ describe('with an "attached" merge request pipeline', () => {
+ let pipeline;
+
+ beforeEach(() => {
+ pipeline = JSON.parse(JSON.stringify(mockPipelineData));
+ pipeline.flags.merge_request_pipeline = true;
+ pipeline.flags.detached_merge_request_pipeline = false;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ stages: [],
+ selectedStage: 'deploy',
+ });
+ });
+
+ it(`renders the pipeline info text like "Pipeline #123 (#12) for !456 with source_branch into target_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for !${
+ pipeline.merge_request.iid
+ } with ${pipeline.merge_request.source_branch} into ${pipeline.merge_request.target_branch}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
+
+ expect(actual).toBe(expected);
+ });
+
+ it(`renders the correct merge request link`, () => {
+ const actual = vm.$el.querySelector('.js-mr-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.path);
+ });
+
+ it(`renders the correct source branch link`, () => {
+ const actual = vm.$el.querySelector('.js-source-branch-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.source_branch_path);
+ });
+
+ it(`renders the correct target branch link`, () => {
+ const actual = vm.$el.querySelector('.js-target-branch-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.target_branch_path);
+ });
});
- it('rendes selected stage', () => {
- expect(vm.$el.querySelector('.dropdown .js-selected-stage').textContent).toContain('deploy');
+ describe('with a detached merge request pipeline', () => {
+ let pipeline;
+
+ beforeEach(() => {
+ pipeline = JSON.parse(JSON.stringify(mockPipelineData));
+ pipeline.flags.merge_request_pipeline = false;
+ pipeline.flags.detached_merge_request_pipeline = true;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ stages: [],
+ selectedStage: 'deploy',
+ });
+ });
+
+ it(`renders the pipeline info like "Pipeline #123 (#12) for !456 with source_branch"`, () => {
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) for !${
+ pipeline.merge_request.iid
+ } with ${pipeline.merge_request.source_branch}`;
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info').innerText);
+
+ expect(actual).toBe(expected);
+ });
+
+ it(`renders the correct merge request link`, () => {
+ const actual = vm.$el.querySelector('.js-mr-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.path);
+ });
+
+ it(`renders the correct source branch link`, () => {
+ const actual = vm.$el.querySelector('.js-source-branch-link').href;
+
+ expect(actual).toContain(pipeline.merge_request.source_branch_path);
+ });
});
});
diff --git a/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js
new file mode 100644
index 00000000000..68fcb321214
--- /dev/null
+++ b/spec/javascripts/jobs/components/unmet_prerequisites_block_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import component from '~/jobs/components/unmet_prerequisites_block.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Unmet Prerequisites Block Job component', () => {
+ const Component = Vue.extend(component);
+ let vm;
+ const helpPath = '/user/project/clusters/index.html#troubleshooting-failed-deployment-jobs';
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ hasNoRunnersForProject: true,
+ helpPath,
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders an alert with the correct message', () => {
+ const container = vm.$el.querySelector('.js-failed-unmet-prerequisites');
+ const alertMessage =
+ 'This job failed because the necessary resources were not successfully created.';
+
+ expect(container).not.toBeNull();
+ expect(container.innerHTML).toContain(alertMessage);
+ });
+
+ it('renders link to help page', () => {
+ const helpLink = vm.$el.querySelector('.js-help-path');
+
+ expect(helpLink).not.toBeNull();
+ expect(helpLink.innerHTML).toContain('More information');
+ expect(helpLink.getAttribute('href')).toEqual(helpPath);
+ });
+});
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 0398f184c0a..88b0bb206ee 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -3,140 +3,6 @@ import { TEST_HOST } from 'spec/test_constants';
const threeWeeksAgo = new Date();
threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
-export default {
- id: 4757,
- name: 'test',
- build_path: '/root/ci-mock/-/jobs/4757',
- retry_path: '/root/ci-mock/-/jobs/4757/retry',
- cancel_path: '/root/ci-mock/-/jobs/4757/cancel',
- new_issue_path: '/root/ci-mock/issues/new',
- playable: false,
- created_at: threeWeeksAgo.toISOString(),
- updated_at: threeWeeksAgo.toISOString(),
- finished_at: threeWeeksAgo.toISOString(),
- queued: 9.54,
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: `${TEST_HOST}/root/ci-mock/-/jobs/4757`,
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- action: {
- icon: 'retry',
- title: 'Retry',
- path: '/root/ci-mock/-/jobs/4757/retry',
- method: 'post',
- },
- },
- coverage: 20,
- erased_at: threeWeeksAgo.toISOString(),
- erased: false,
- duration: 6.785563,
- tags: ['tag'],
- user: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- erase_path: '/root/ci-mock/-/jobs/4757/erase',
- artifacts: [null],
- runner: {
- id: 1,
- description: 'local ci runner',
- edit_path: '/root/ci-mock/runners/1/edit',
- },
- pipeline: {
- id: 140,
- user: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- active: false,
- coverage: null,
- source: 'unknown',
- created_at: '2017-05-24T09:59:58.634Z',
- updated_at: '2017-06-01T17:32:00.062Z',
- path: '/root/ci-mock/pipelines/140',
- flags: {
- latest: true,
- stuck: false,
- yaml_errors: false,
- retryable: false,
- cancelable: false,
- },
- details: {
- status: {
- icon: 'status_success',
- text: 'passed',
- label: 'passed',
- group: 'success',
- has_details: true,
- details_path: '/root/ci-mock/pipelines/140',
- favicon:
- '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
- },
- duration: 6,
- finished_at: '2017-06-01T17:32:00.042Z',
- },
- ref: {
- name: 'abc',
- path: '/root/ci-mock/commits/abc',
- tag: false,
- branch: true,
- },
- commit: {
- id: 'c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
- short_id: 'c5864777',
- title: 'Add new file',
- created_at: '2017-05-24T10:59:52.000+01:00',
- parent_ids: ['798e5f902592192afaba73f4668ae30e56eae492'],
- message: 'Add new file',
- author_name: 'Root',
- author_email: 'admin@example.com',
- authored_date: '2017-05-24T10:59:52.000+01:00',
- committer_name: 'Root',
- committer_email: 'admin@example.com',
- committed_date: '2017-05-24T10:59:52.000+01:00',
- author: {
- name: 'Root',
- username: 'root',
- id: 1,
- state: 'active',
- avatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- web_url: 'http://localhost:3000/root',
- },
- author_gravatar_url:
- 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
- commit_url:
- 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
- commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
- },
- },
- metadata: {
- timeout_human_readable: '1m 40s',
- timeout_source: 'runner',
- },
- merge_request: {
- iid: 2,
- path: '/root/ci-mock/merge_requests/2',
- },
- raw_path: '/root/ci-mock/builds/4757/raw',
- has_trace: true,
-};
-
export const stages = [
{
name: 'build',
@@ -678,7 +544,7 @@ export const stages = [
icon: 'status_warning',
text: 'failed',
label: 'failed (allowed to fail)',
- group: 'failed_with_warnings',
+ group: 'failed-with-warnings',
tooltip: 'failed - (unknown failure) (allowed to fail)',
has_details: true,
details_path: '/gitlab-org/gitlab-shell/-/jobs/454',
@@ -710,7 +576,7 @@ export const stages = [
icon: 'status_warning',
text: 'failed',
label: 'failed (allowed to fail)',
- group: 'failed_with_warnings',
+ group: 'failed-with-warnings',
tooltip: 'failed - (unknown failure) (allowed to fail)',
has_details: true,
details_path: '/gitlab-org/gitlab-shell/-/jobs/454',
@@ -738,7 +604,7 @@ export const stages = [
icon: 'status_warning',
text: 'passed',
label: 'passed with warnings',
- group: 'success_with_warnings',
+ group: 'success-with-warnings',
tooltip: 'passed',
has_details: true,
details_path: '/gitlab-org/gitlab-shell/pipelines/27#test',
@@ -1043,6 +909,168 @@ export const stages = [
},
];
+export default {
+ id: 4757,
+ name: 'test',
+ build_path: '/root/ci-mock/-/jobs/4757',
+ retry_path: '/root/ci-mock/-/jobs/4757/retry',
+ cancel_path: '/root/ci-mock/-/jobs/4757/cancel',
+ new_issue_path: '/root/ci-mock/issues/new',
+ playable: false,
+ created_at: threeWeeksAgo.toISOString(),
+ updated_at: threeWeeksAgo.toISOString(),
+ finished_at: threeWeeksAgo.toISOString(),
+ queued: 9.54,
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: `${TEST_HOST}/root/ci-mock/-/jobs/4757`,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ action: {
+ icon: 'retry',
+ title: 'Retry',
+ path: '/root/ci-mock/-/jobs/4757/retry',
+ method: 'post',
+ },
+ },
+ coverage: 20,
+ erased_at: threeWeeksAgo.toISOString(),
+ erased: false,
+ duration: 6.785563,
+ tags: ['tag'],
+ user: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ erase_path: '/root/ci-mock/-/jobs/4757/erase',
+ artifacts: [null],
+ runner: {
+ id: 1,
+ description: 'local ci runner',
+ edit_path: '/root/ci-mock/runners/1/edit',
+ },
+ pipeline: {
+ id: 140,
+ iid: 13,
+ user: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ active: false,
+ coverage: null,
+ source: 'unknown',
+ created_at: '2017-05-24T09:59:58.634Z',
+ updated_at: '2017-06-01T17:32:00.062Z',
+ path: '/root/ci-mock/pipelines/140',
+ flags: {
+ latest: true,
+ stuck: false,
+ yaml_errors: false,
+ retryable: false,
+ cancelable: false,
+ },
+ details: {
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ has_details: true,
+ details_path: '/root/ci-mock/pipelines/140',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png',
+ },
+ duration: 6,
+ finished_at: '2017-06-01T17:32:00.042Z',
+ stages: [
+ {
+ dropdown_path: '/jashkenas/underscore/pipelines/16/stage.json?stage=build',
+ name: 'build',
+ path: '/jashkenas/underscore/pipelines/16#build',
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ },
+ title: 'build: passed',
+ },
+ {
+ dropdown_path: '/jashkenas/underscore/pipelines/16/stage.json?stage=test',
+ name: 'test',
+ path: '/jashkenas/underscore/pipelines/16#test',
+ status: {
+ icon: 'status_warning',
+ text: 'passed',
+ label: 'passed with warnings',
+ group: 'success-with-warnings',
+ },
+ title: 'test: passed with warnings',
+ },
+ ],
+ },
+ ref: {
+ name: 'abc',
+ path: '/root/ci-mock/commits/abc',
+ tag: false,
+ branch: true,
+ },
+ commit: {
+ id: 'c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
+ short_id: 'c5864777',
+ title: 'Add new file',
+ created_at: '2017-05-24T10:59:52.000+01:00',
+ parent_ids: ['798e5f902592192afaba73f4668ae30e56eae492'],
+ message: 'Add new file',
+ author_name: 'Root',
+ author_email: 'admin@example.com',
+ authored_date: '2017-05-24T10:59:52.000+01:00',
+ committer_name: 'Root',
+ committer_email: 'admin@example.com',
+ committed_date: '2017-05-24T10:59:52.000+01:00',
+ author: {
+ name: 'Root',
+ username: 'root',
+ id: 1,
+ state: 'active',
+ avatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3000/root',
+ },
+ author_gravatar_url:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ commit_url:
+ 'http://localhost:3000/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
+ commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
+ },
+ },
+ metadata: {
+ timeout_human_readable: '1m 40s',
+ timeout_source: 'runner',
+ },
+ merge_request: {
+ iid: 2,
+ path: '/root/ci-mock/merge_requests/2',
+ },
+ raw_path: '/root/ci-mock/builds/4757/raw',
+ has_trace: true,
+};
+
export const jobsInStage = {
name: 'build',
title: 'build: running',
diff --git a/spec/javascripts/jobs/store/actions_spec.js b/spec/javascripts/jobs/store/actions_spec.js
index 77b44995b12..7b96df85b82 100644
--- a/spec/javascripts/jobs/store/actions_spec.js
+++ b/spec/javascripts/jobs/store/actions_spec.js
@@ -16,10 +16,6 @@ import {
stopPollingTrace,
receiveTraceSuccess,
receiveTraceError,
- requestStages,
- fetchStages,
- receiveStagesSuccess,
- receiveStagesError,
requestJobsForStage,
fetchJobsForStage,
receiveJobsForStageSuccess,
@@ -307,107 +303,6 @@ describe('Job State actions', () => {
});
});
- describe('requestStages', () => {
- it('should commit REQUEST_STAGES mutation ', done => {
- testAction(requestStages, null, mockedState, [{ type: types.REQUEST_STAGES }], [], done);
- });
- });
-
- describe('fetchStages', () => {
- let mock;
-
- beforeEach(() => {
- mockedState.job.pipeline = {
- path: `${TEST_HOST}/endpoint`,
- };
- mockedState.selectedStage = 'deploy';
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('success', () => {
- it('dispatches requestStages and receiveStagesSuccess, fetchJobsForStage ', done => {
- mock
- .onGet(`${TEST_HOST}/endpoint.json`)
- .replyOnce(200, { details: { stages: [{ name: 'build' }, { name: 'deploy' }] } });
-
- testAction(
- fetchStages,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestStages',
- },
- {
- payload: [{ name: 'build' }, { name: 'deploy' }],
- type: 'receiveStagesSuccess',
- },
- {
- payload: { name: 'deploy' },
- type: 'fetchJobsForStage',
- },
- ],
- done,
- );
- });
- });
-
- describe('error', () => {
- beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
- });
-
- it('dispatches requestStages and receiveStagesError ', done => {
- testAction(
- fetchStages,
- null,
- mockedState,
- [],
- [
- {
- type: 'requestStages',
- },
- {
- type: 'receiveStagesError',
- },
- ],
- done,
- );
- });
- });
- });
-
- describe('receiveStagesSuccess', () => {
- it('should commit RECEIVE_STAGES_SUCCESS mutation ', done => {
- testAction(
- receiveStagesSuccess,
- {},
- mockedState,
- [{ type: types.RECEIVE_STAGES_SUCCESS, payload: {} }],
- [],
- done,
- );
- });
- });
-
- describe('receiveStagesError', () => {
- it('should commit RECEIVE_STAGES_ERROR mutation ', done => {
- testAction(
- receiveStagesError,
- null,
- mockedState,
- [{ type: types.RECEIVE_STAGES_ERROR }],
- [],
- done,
- );
- });
- });
-
describe('requestJobsForStage', () => {
it('should commit REQUEST_JOBS_FOR_STAGE mutation ', done => {
testAction(
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index e5678ee5379..ccf439aac74 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -16,10 +16,10 @@ let saveLabelCount = 0;
let mock;
describe('Issue dropdown sidebar', () => {
- preloadFixtures('static/issue_sidebar_label.html.raw');
+ preloadFixtures('static/issue_sidebar_label.html');
beforeEach(() => {
- loadFixtures('static/issue_sidebar_label.html.raw');
+ loadFixtures('static/issue_sidebar_label.html');
mock = new MockAdapter(axios);
diff --git a/spec/javascripts/labels_select_spec.js b/spec/javascripts/labels_select_spec.js
deleted file mode 100644
index acfdc885032..00000000000
--- a/spec/javascripts/labels_select_spec.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import $ from 'jquery';
-import LabelsSelect from '~/labels_select';
-
-const mockUrl = '/foo/bar/url';
-
-const mockLabels = [
- {
- id: 26,
- title: 'Foo Label',
- description: 'Foobar',
- color: '#BADA55',
- text_color: '#FFFFFF',
- },
-];
-
-describe('LabelsSelect', () => {
- describe('getLabelTemplate', () => {
- const label = mockLabels[0];
- let $labelEl;
-
- beforeEach(() => {
- $labelEl = $(
- LabelsSelect.getLabelTemplate({
- labels: mockLabels,
- issueUpdateURL: mockUrl,
- }),
- );
- });
-
- it('generated label item template has correct label URL', () => {
- expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
- });
-
- it('generated label item template has correct label title', () => {
- expect($labelEl.find('span.label').text()).toBe(label.title);
- });
-
- it('generated label item template has label description as title attribute', () => {
- expect($labelEl.find('span.label').attr('title')).toBe(label.description);
- });
-
- it('generated label item template has correct label styles', () => {
- expect($labelEl.find('span.label').attr('style')).toBe(
- `background-color: ${label.color}; color: ${label.text_color};`,
- );
- });
-
- it('generated label item has a badge class', () => {
- expect($labelEl.find('span').hasClass('badge')).toEqual(true);
- });
- });
-});
diff --git a/spec/javascripts/lazy_loader_spec.js b/spec/javascripts/lazy_loader_spec.js
index cbdc1644430..f3fb792c62d 100644
--- a/spec/javascripts/lazy_loader_spec.js
+++ b/spec/javascripts/lazy_loader_spec.js
@@ -11,11 +11,11 @@ const execImmediately = callback => {
describe('LazyLoader', function() {
let lazyLoader = null;
- preloadFixtures('issues/issue_with_comment.html.raw');
+ preloadFixtures('issues/issue_with_comment.html');
describe('without IntersectionObserver', () => {
beforeEach(function() {
- loadFixtures('issues/issue_with_comment.html.raw');
+ loadFixtures('issues/issue_with_comment.html');
lazyLoader = new LazyLoader({
observerNode: 'foobar',
@@ -131,7 +131,7 @@ describe('LazyLoader', function() {
describe('with IntersectionObserver', () => {
beforeEach(function() {
- loadFixtures('issues/issue_with_comment.html.raw');
+ loadFixtures('issues/issue_with_comment.html');
lazyLoader = new LazyLoader({
observerNode: 'foobar',
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 0bb43c94f6a..296ee85089f 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -2,6 +2,7 @@ import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import MockAdapter from 'axios-mock-adapter';
import { faviconDataUrl, overlayDataUrl, faviconWithOverlayDataUrl } from './mock_data';
+import breakpointInstance from '~/breakpoints';
const PIXEL_TOLERANCE = 0.2;
@@ -380,6 +381,38 @@ describe('common_utils', () => {
});
});
+ describe('contentTop', () => {
+ it('does not add height for fileTitle or compareVersionsHeader if screen is too small', () => {
+ spyOn(breakpointInstance, 'isDesktop').and.returnValue(false);
+
+ setFixtures(`
+ <div class="diff-file file-title-flex-parent">
+ blah blah blah
+ </div>
+ <div class="mr-version-controls">
+ more blah blah blah
+ </div>
+ `);
+
+ expect(commonUtils.contentTop()).toBe(0);
+ });
+
+ it('adds height for fileTitle and compareVersionsHeader screen is large enough', () => {
+ spyOn(breakpointInstance, 'isDesktop').and.returnValue(true);
+
+ setFixtures(`
+ <div class="diff-file file-title-flex-parent">
+ blah blah blah
+ </div>
+ <div class="mr-version-controls">
+ more blah blah blah
+ </div>
+ `);
+
+ expect(commonUtils.contentTop()).toBe(18);
+ });
+ });
+
describe('parseBoolean', () => {
const { parseBoolean } = commonUtils;
@@ -819,20 +852,20 @@ describe('common_utils', () => {
describe('roundOffFloat', () => {
it('Rounds off decimal places of a float number with provided precision', () => {
- expect(commonUtils.roundOffFloat(3.141592, 3)).toBe(3.142);
+ expect(commonUtils.roundOffFloat(3.141592, 3)).toBeCloseTo(3.142);
});
it('Rounds off a float number to a whole number when provided precision is zero', () => {
- expect(commonUtils.roundOffFloat(3.141592, 0)).toBe(3);
- expect(commonUtils.roundOffFloat(3.5, 0)).toBe(4);
+ expect(commonUtils.roundOffFloat(3.141592, 0)).toBeCloseTo(3);
+ expect(commonUtils.roundOffFloat(3.5, 0)).toBeCloseTo(4);
});
it('Rounds off float number to nearest 0, 10, 100, 1000 and so on when provided precision is below 0', () => {
- expect(commonUtils.roundOffFloat(34567.14159, -1)).toBe(34570);
- expect(commonUtils.roundOffFloat(34567.14159, -2)).toBe(34600);
- expect(commonUtils.roundOffFloat(34567.14159, -3)).toBe(35000);
- expect(commonUtils.roundOffFloat(34567.14159, -4)).toBe(30000);
- expect(commonUtils.roundOffFloat(34567.14159, -5)).toBe(0);
+ expect(commonUtils.roundOffFloat(34567.14159, -1)).toBeCloseTo(34570);
+ expect(commonUtils.roundOffFloat(34567.14159, -2)).toBeCloseTo(34600);
+ expect(commonUtils.roundOffFloat(34567.14159, -3)).toBeCloseTo(35000);
+ expect(commonUtils.roundOffFloat(34567.14159, -4)).toBeCloseTo(30000);
+ expect(commonUtils.roundOffFloat(34567.14159, -5)).toBeCloseTo(0);
});
});
@@ -861,4 +894,14 @@ describe('common_utils', () => {
expect(commonUtils.isInViewport(el)).toBe(false);
});
});
+
+ describe('isScopedLabel', () => {
+ it('returns true when `::` is present in title', () => {
+ expect(commonUtils.isScopedLabel({ title: 'foo::bar' })).toBe(true);
+ });
+
+ it('returns false when `::` is not present', () => {
+ expect(commonUtils.isScopedLabel({ title: 'foobar' })).toBe(false);
+ });
+ });
});
diff --git a/spec/javascripts/lib/utils/higlight_spec.js b/spec/javascripts/lib/utils/higlight_spec.js
new file mode 100644
index 00000000000..638bbf65ae9
--- /dev/null
+++ b/spec/javascripts/lib/utils/higlight_spec.js
@@ -0,0 +1,43 @@
+import highlight from '~/lib/utils/highlight';
+
+describe('highlight', () => {
+ it(`should appropriately surround substring matches`, () => {
+ const expected = 'g<b>i</b><b>t</b>lab';
+
+ expect(highlight('gitlab', 'it')).toBe(expected);
+ });
+
+ it(`should return an empty string in the case of invalid inputs`, () => {
+ [null, undefined].forEach(input => {
+ expect(highlight(input, 'match')).toBe('');
+ });
+ });
+
+ it(`should return the original value if match is null, undefined, or ''`, () => {
+ [null, undefined].forEach(match => {
+ expect(highlight('gitlab', match)).toBe('gitlab');
+ });
+ });
+
+ it(`should highlight matches in non-string inputs`, () => {
+ const expected = '123<b>4</b><b>5</b>6';
+
+ expect(highlight(123456, 45)).toBe(expected);
+ });
+
+ it(`should sanitize the input string before highlighting matches`, () => {
+ const expected = 'hello <b>w</b>orld';
+
+ expect(highlight('hello <b>world</b>', 'w')).toBe(expected);
+ });
+
+ it(`should not highlight anything if no matches are found`, () => {
+ expect(highlight('gitlab', 'hello')).toBe('gitlab');
+ });
+
+ it(`should allow wrapping elements to be customized`, () => {
+ const expected = '1<hello>2</hello>3';
+
+ expect(highlight('123', '2', '<hello>', '</hello>')).toBe(expected);
+ });
+});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index 4eea364bd69..a75470b4db8 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -5,7 +5,7 @@ import LineHighlighter from '~/line_highlighter';
describe('LineHighlighter', function() {
var clickLine;
- preloadFixtures('static/line_highlighter.html.raw');
+ preloadFixtures('static/line_highlighter.html');
clickLine = function(number, eventData = {}) {
if ($.isEmptyObject(eventData)) {
return $('#L' + number).click();
@@ -15,7 +15,7 @@ describe('LineHighlighter', function() {
}
};
beforeEach(function() {
- loadFixtures('static/line_highlighter.html.raw');
+ loadFixtures('static/line_highlighter.html');
this['class'] = new LineHighlighter();
this.css = this['class'].highlightLineClass;
return (this.spies = {
diff --git a/spec/javascripts/matchers.js b/spec/javascripts/matchers.js
index 406527b08a3..7d1921cabcf 100644
--- a/spec/javascripts/matchers.js
+++ b/spec/javascripts/matchers.js
@@ -28,7 +28,7 @@ export default {
reference.getAttribute('xlink:href').endsWith(`#${iconName}`),
);
const result = {
- pass: !!matchingIcon,
+ pass: Boolean(matchingIcon),
};
if (result.pass) {
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index ab809930804..cadcc15385f 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -11,9 +11,9 @@ describe('MergeRequest', function() {
describe('task lists', function() {
let mock;
- preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ preloadFixtures('merge_requests/merge_request_with_task_list.html');
beforeEach(function() {
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ loadFixtures('merge_requests/merge_request_with_task_list.html');
spyOn(axios, 'patch').and.callThrough();
mock = new MockAdapter(axios);
@@ -58,7 +58,7 @@ describe('MergeRequest', function() {
{
merge_request: {
description: '- [ ] Task List Item',
- lock_version: undefined,
+ lock_version: 0,
update_task: { line_number: lineNumber, line_source: lineSource, index, checked },
},
},
@@ -125,7 +125,7 @@ describe('MergeRequest', function() {
describe('hideCloseButton', () => {
describe('merge request of another user', () => {
beforeEach(() => {
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ loadFixtures('merge_requests/merge_request_with_task_list.html');
this.el = document.querySelector('.js-issuable-actions');
new MergeRequest(); // eslint-disable-line no-new
MergeRequest.hideCloseButton();
@@ -145,7 +145,7 @@ describe('MergeRequest', function() {
describe('merge request of current_user', () => {
beforeEach(() => {
- loadFixtures('merge_requests/merge_request_of_current_user.html.raw');
+ loadFixtures('merge_requests/merge_request_of_current_user.html');
this.el = document.querySelector('.js-issuable-actions');
MergeRequest.hideCloseButton();
});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index c8df05eccf5..1295d900de7 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -22,8 +22,8 @@ describe('MergeRequestTabs', function() {
};
preloadFixtures(
- 'merge_requests/merge_request_with_task_list.html.raw',
- 'merge_requests/diff_comment.html.raw',
+ 'merge_requests/merge_request_with_task_list.html',
+ 'merge_requests/diff_comment.html',
);
beforeEach(function() {
@@ -48,7 +48,7 @@ describe('MergeRequestTabs', function() {
var windowTarget = '_blank';
beforeEach(function() {
- loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+ loadFixtures('merge_requests/merge_request_with_task_list.html');
tabUrl = $('.commits-tab a').attr('href');
});
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
index 092ca9e1dab..aa4a376caf7 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -5,10 +5,10 @@ import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Mini Pipeline Graph Dropdown', () => {
- preloadFixtures('static/mini_dropdown_graph.html.raw');
+ preloadFixtures('static/mini_dropdown_graph.html');
beforeEach(() => {
- loadFixtures('static/mini_dropdown_graph.html.raw');
+ loadFixtures('static/mini_dropdown_graph.html');
});
describe('When is initialized', () => {
diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js
index d334ef7ba4f..56609665b88 100644
--- a/spec/javascripts/monitoring/charts/area_spec.js
+++ b/spec/javascripts/monitoring/charts/area_spec.js
@@ -1,28 +1,31 @@
import { shallowMount } from '@vue/test-utils';
-import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { shallowWrapperContainsSlotText } from 'spec/helpers/vue_test_utils_helper';
import Area from '~/monitoring/components/charts/area.vue';
-import MonitoringStore from '~/monitoring/stores/monitoring_store';
+import { createStore } from '~/monitoring/stores';
+import * as types from '~/monitoring/stores/mutation_types';
import MonitoringMock, { deploymentData } from '../mock_data';
describe('Area component', () => {
const mockWidgets = 'mockWidgets';
+ const mockSvgPathContent = 'mockSvgPathContent';
let mockGraphData;
let areaChart;
let spriteSpy;
beforeEach(() => {
- const store = new MonitoringStore();
- store.storeMetrics(MonitoringMock.data);
- store.storeDeploymentData(deploymentData);
+ const store = createStore();
- [mockGraphData] = store.groups[0].metrics;
+ store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
+ store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
+
+ [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
areaChart = shallowMount(Area, {
propsData: {
graphData: mockGraphData,
containerWidth: 0,
- deploymentData: store.deploymentData,
+ deploymentData: store.state.monitoringDashboard.deploymentData,
},
slots: {
default: mockWidgets,
@@ -30,7 +33,7 @@ describe('Area component', () => {
});
spriteSpy = spyOnDependency(Area, 'getSvgIconPathContent').and.callFake(
- () => new Promise(resolve => resolve()),
+ () => new Promise(resolve => resolve(mockSvgPathContent)),
);
});
@@ -64,7 +67,7 @@ describe('Area component', () => {
expect(props.data).toBe(areaChart.vm.chartData);
expect(props.option).toBe(areaChart.vm.chartOptions);
expect(props.formatTooltipText).toBe(areaChart.vm.formatTooltipText);
- expect(props.thresholds).toBe(areaChart.props('alertData'));
+ expect(props.thresholds).toBe(areaChart.vm.thresholds);
});
it('recieves a tooltip title', () => {
@@ -74,15 +77,6 @@ describe('Area component', () => {
expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipTitle', mockTitle)).toBe(true);
});
- it('recieves tooltip content', () => {
- const mockContent = 'mockContent';
- areaChart.vm.tooltip.content = mockContent;
-
- expect(shallowWrapperContainsSlotText(glAreaChart, 'tooltipContent', mockContent)).toBe(
- true,
- );
- });
-
describe('when tooltip is showing deployment data', () => {
beforeEach(() => {
areaChart.vm.tooltip.isDeployment = true;
@@ -110,14 +104,16 @@ describe('Area component', () => {
const generateSeriesData = type => ({
seriesData: [
{
+ seriesName: areaChart.vm.chartData[0].name,
componentSubType: type,
value: [mockDate, 5.55555],
+ seriesIndex: 0,
},
],
value: mockDate,
});
- describe('series is of line type', () => {
+ describe('when series is of line type', () => {
beforeEach(() => {
areaChart.vm.formatTooltipText(generateSeriesData('line'));
});
@@ -127,11 +123,20 @@ describe('Area component', () => {
});
it('formats tooltip content', () => {
- expect(areaChart.vm.tooltip.content).toBe('CPU 5.556');
+ const name = 'Core Usage';
+ const value = '5.556';
+ const seriesLabel = areaChart.find(GlChartSeriesLabel);
+
+ expect(seriesLabel.vm.color).toBe('');
+ expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
+ expect(areaChart.vm.tooltip.content).toEqual([{ name, value, color: undefined }]);
+ expect(
+ shallowWrapperContainsSlotText(areaChart.find(GlAreaChart), 'tooltipContent', value),
+ ).toBe(true);
});
});
- describe('series is of scatter type', () => {
+ describe('when series is of scatter type', () => {
beforeEach(() => {
areaChart.vm.formatTooltipText(generateSeriesData('scatter'));
});
@@ -146,24 +151,31 @@ describe('Area component', () => {
});
});
- describe('getScatterSymbol', () => {
+ describe('setSvg', () => {
+ const mockSvgName = 'mockSvgName';
+
beforeEach(() => {
- areaChart.vm.getScatterSymbol();
+ areaChart.vm.setSvg(mockSvgName);
});
- it('gets rocket svg path content for use as deployment data symbol', () => {
- expect(spriteSpy).toHaveBeenCalledWith('rocket');
+ it('gets svg path content', () => {
+ expect(spriteSpy).toHaveBeenCalledWith(mockSvgName);
+ });
+
+ it('sets svg path content', done => {
+ areaChart.vm.$nextTick(() => {
+ expect(areaChart.vm.svgs[mockSvgName]).toBe(`path://${mockSvgPathContent}`);
+ done();
+ });
});
});
describe('onResize', () => {
const mockWidth = 233;
- const mockHeight = 144;
beforeEach(() => {
spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
width: mockWidth,
- height: mockHeight,
}));
areaChart.vm.onResize();
});
@@ -171,28 +183,35 @@ describe('Area component', () => {
it('sets area chart width', () => {
expect(areaChart.vm.width).toBe(mockWidth);
});
-
- it('sets area chart height', () => {
- expect(areaChart.vm.height).toBe(mockHeight);
- });
});
});
describe('computed', () => {
describe('chartData', () => {
+ let chartData;
+ const seriesData = () => chartData[0];
+
+ beforeEach(() => {
+ ({ chartData } = areaChart.vm);
+ });
+
it('utilizes all data points', () => {
- expect(Object.keys(areaChart.vm.chartData)).toEqual(['Cores']);
- expect(areaChart.vm.chartData.Cores.length).toBe(297);
+ expect(chartData.length).toBe(1);
+ expect(seriesData().data.length).toBe(297);
});
it('creates valid data', () => {
- const data = areaChart.vm.chartData.Cores;
+ const { data } = seriesData();
expect(
data.filter(([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number')
.length,
).toBe(data.length);
});
+
+ it('formats line width correctly', () => {
+ expect(chartData[0].lineStyle.width).toBe(2);
+ });
});
describe('scatterSeries', () => {
@@ -205,12 +224,6 @@ describe('Area component', () => {
});
});
- describe('xAxisLabel', () => {
- it('constructs a label for the chart x-axis', () => {
- expect(areaChart.vm.xAxisLabel).toBe('Core Usage');
- });
- });
-
describe('yAxisLabel', () => {
it('constructs a label for the chart y-axis', () => {
expect(areaChart.vm.yAxisLabel).toBe('CPU');
diff --git a/spec/javascripts/monitoring/charts/single_stat_spec.js b/spec/javascripts/monitoring/charts/single_stat_spec.js
new file mode 100644
index 00000000000..12b73002f97
--- /dev/null
+++ b/spec/javascripts/monitoring/charts/single_stat_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
+
+describe('Single Stat Chart component', () => {
+ let singleStatChart;
+
+ beforeEach(() => {
+ singleStatChart = shallowMount(SingleStatChart, {
+ propsData: {
+ title: 'Time to render',
+ value: 1,
+ unit: 'sec',
+ },
+ });
+ });
+
+ afterEach(() => {
+ singleStatChart.destroy();
+ });
+
+ describe('computed', () => {
+ describe('valueWithUnit', () => {
+ it('should interpolate the value and unit props', () => {
+ expect(singleStatChart.vm.valueWithUnit).toBe('1sec');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index b1778029a77..1a371c3adaf 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -1,8 +1,16 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
+import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
+import * as types from '~/monitoring/stores/mutation_types';
+import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
-import { metricsGroupsAPIResponse, mockApiEndpoint, environmentData } from './mock_data';
+import {
+ metricsGroupsAPIResponse,
+ mockApiEndpoint,
+ environmentData,
+ singleGroupResponse,
+} from './mock_data';
const propsData = {
hasMetrics: false,
@@ -19,6 +27,9 @@ const propsData = {
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
environmentsEndpoint: '/root/hello-prometheus/environments/35',
currentEnvironmentName: 'production',
+ customMetricsAvailable: false,
+ customMetricsPath: '',
+ validateQueryPath: '',
};
export default propsData;
@@ -26,6 +37,8 @@ export default propsData;
describe('Dashboard', () => {
let DashboardComponent;
let mock;
+ let store;
+ let component;
beforeEach(() => {
setFixtures(`
@@ -33,23 +46,34 @@ describe('Dashboard', () => {
<div class="layout-page"></div>
`);
+ window.gon = {
+ ...window.gon,
+ ee: false,
+ features: {
+ grafanaDashboardLink: true,
+ },
+ };
+
+ store = createStore();
mock = new MockAdapter(axios);
DashboardComponent = Vue.extend(Dashboard);
});
afterEach(() => {
+ component.$destroy();
mock.restore();
});
describe('no metrics are available yet', () => {
it('shows a getting started empty state when no metrics are present', () => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData,
+ propsData: { ...propsData },
+ store,
});
expect(component.$el.querySelector('.prometheus-graphs')).toBe(null);
- expect(component.state).toEqual('gettingStarted');
+ expect(component.emptyState).toEqual('gettingStarted');
});
});
@@ -59,21 +83,27 @@ describe('Dashboard', () => {
});
it('shows up a loading state', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: { ...propsData, hasMetrics: true },
+ store,
});
Vue.nextTick(() => {
- expect(component.state).toEqual('loading');
+ expect(component.emptyState).toEqual('loading');
done();
});
});
it('hides the legend when showLegend is false', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showLegend: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showLegend: false,
+ },
+ store,
});
setTimeout(() => {
@@ -85,9 +115,14 @@ describe('Dashboard', () => {
});
it('hides the group panels when showPanels is false', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ },
+ store,
});
setTimeout(() => {
@@ -98,55 +133,210 @@ describe('Dashboard', () => {
});
});
- it('renders the dropdown with a number of environments', done => {
- const component = new DashboardComponent({
+ it('renders the environments dropdown with a number of environments', done => {
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ },
+ store,
});
- component.store.storeEnvironmentsData(environmentData);
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ singleGroupResponse,
+ );
setTimeout(() => {
- const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul li a');
+ const dropdownMenuEnvironments = component.$el.querySelectorAll(
+ '.js-environments-dropdown .dropdown-item',
+ );
- expect(dropdownMenuEnvironments.length).toEqual(component.store.environmentsData.length);
+ expect(dropdownMenuEnvironments.length).toEqual(component.environments.length);
done();
});
});
- it('hides the dropdown list when there is no environments', done => {
- const component = new DashboardComponent({
+ it('hides the environments dropdown list when there is no environments', done => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ },
+ store,
+ });
+
+ component.$store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, []);
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ singleGroupResponse,
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ const dropdownMenuEnvironments = component.$el.querySelectorAll(
+ '.js-environments-dropdown .dropdown-item',
+ );
+
+ expect(dropdownMenuEnvironments.length).toEqual(0);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('renders the environments dropdown with a single active element', done => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ },
+ store,
+ });
+
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ singleGroupResponse,
+ );
+
+ Vue.nextTick()
+ .then(() => {
+ const dropdownItems = component.$el.querySelectorAll(
+ '.js-environments-dropdown .dropdown-item[active="true"]',
+ );
+
+ expect(dropdownItems.length).toEqual(1);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('hides the dropdown', done => {
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ environmentsEndpoint: '',
+ },
+ store,
+ });
+
+ Vue.nextTick(() => {
+ const dropdownIsActiveElement = component.$el.querySelectorAll('.environments');
+
+ expect(dropdownIsActiveElement.length).toEqual(0);
+ done();
});
+ });
- component.store.storeEnvironmentsData([]);
+ it('renders the time window dropdown with a set of options', done => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ },
+ store,
+ });
+ const numberOfTimeWindows = Object.keys(timeWindows).length;
setTimeout(() => {
- const dropdownMenuEnvironments = component.$el.querySelectorAll('.dropdown-menu ul');
+ const timeWindowDropdown = component.$el.querySelector('.js-time-window-dropdown');
+ const timeWindowDropdownEls = component.$el.querySelectorAll(
+ '.js-time-window-dropdown .dropdown-item',
+ );
+
+ expect(timeWindowDropdown).not.toBeNull();
+ expect(timeWindowDropdownEls.length).toEqual(numberOfTimeWindows);
- expect(dropdownMenuEnvironments.length).toEqual(0);
done();
});
});
- it('renders the dropdown with a single is-active element', done => {
- const component = new DashboardComponent({
+ it('fetches the metrics data with proper time window', done => {
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ },
+ store,
});
- component.store.storeEnvironmentsData(environmentData);
+ spyOn(component.$store, 'dispatch').and.stub();
+ const getTimeDiffSpy = spyOnDependency(Dashboard, 'getTimeDiff');
+
+ component.$store.commit(
+ `monitoringDashboard/${types.SET_ENVIRONMENTS_ENDPOINT}`,
+ '/environments',
+ );
+ component.$store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
+
+ component.$mount();
+
+ Vue.nextTick()
+ .then(() => {
+ expect(component.$store.dispatch).toHaveBeenCalled();
+ expect(getTimeDiffSpy).toHaveBeenCalledWith(component.selectedTimeWindow);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('shows a specific time window selected from the url params', done => {
+ spyOnDependency(Dashboard, 'getParameterValues').and.returnValue(['thirtyMinutes']);
+
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, hasMetrics: true },
+ store,
+ });
setTimeout(() => {
- const dropdownIsActiveElement = component.$el.querySelectorAll(
- '.dropdown-menu ul li a.is-active',
+ const selectedTimeWindow = component.$el.querySelector(
+ '.js-time-window-dropdown [active="true"]',
);
- expect(dropdownIsActiveElement.length).toEqual(1);
- expect(dropdownIsActiveElement[0].textContent.trim()).toEqual(
- component.currentEnvironmentName,
- );
+ expect(selectedTimeWindow.textContent.trim()).toEqual('30 minutes');
+ done();
+ });
+ });
+
+ it('defaults to the eight hours time window for non valid url parameters', done => {
+ spyOnDependency(Dashboard, 'getParameterValues').and.returnValue([
+ '<script>alert("XSS")</script>',
+ ]);
+
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: { ...propsData, hasMetrics: true },
+ store,
+ });
+
+ Vue.nextTick(() => {
+ expect(component.selectedTimeWindowKey).toEqual(timeWindowsKeyNames.eightHours);
+
done();
});
});
@@ -163,9 +353,14 @@ describe('Dashboard', () => {
});
it('sets elWidth to page width when the sidebar is resized', done => {
- const component = new DashboardComponent({
+ component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
- propsData: { ...propsData, hasMetrics: true, showPanels: false },
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ },
+ store,
});
expect(component.elWidth).toEqual(0);
@@ -185,4 +380,57 @@ describe('Dashboard', () => {
.catch(done.fail);
});
});
+
+ describe('external dashboard link', () => {
+ beforeEach(() => {
+ mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
+ });
+
+ describe('with feature flag enabled', () => {
+ beforeEach(() => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ externalDashboardUrl: '/mockUrl',
+ },
+ store,
+ });
+ });
+
+ it('shows the link', done => {
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-external-dashboard-link').innerText).toContain(
+ 'View full dashboard',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('without feature flage enabled', () => {
+ beforeEach(() => {
+ window.gon.features.grafanaDashboardLink = false;
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ externalDashboardUrl: '',
+ },
+ store,
+ });
+ });
+
+ it('does not show the link', done => {
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-external-dashboard-link')).toBe(null);
+ done();
+ });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/monitoring/helpers.js b/spec/javascripts/monitoring/helpers.js
new file mode 100644
index 00000000000..672e3b948c4
--- /dev/null
+++ b/spec/javascripts/monitoring/helpers.js
@@ -0,0 +1,8 @@
+// eslint-disable-next-line import/prefer-default-export
+export const resetStore = store => {
+ store.replaceState({
+ showEmptyState: true,
+ emptyState: 'loading',
+ groups: [],
+ });
+};
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index ffc7148fde2..d9d8cb66749 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -330,6 +330,11 @@ export const metricsGroupsAPIResponse = {
weight: 1,
queries: [
{
+ appearance: {
+ line: {
+ width: 2,
+ },
+ },
query_range:
'avg(rate(container_cpu_usage_seconds_total{%{environment_filter}}[2m])) * 100',
label: 'Core Usage',
@@ -680,6 +685,47 @@ export const metricsGroupsAPIResponse = {
last_update: '2017-05-25T13:18:34.949Z',
};
+export const singleGroupResponse = [
+ {
+ group: 'System metrics (Kubernetes)',
+ priority: 5,
+ metrics: [
+ {
+ title: 'Memory Usage (Total)',
+ weight: 0,
+ y_label: 'Total Memory Used',
+ queries: [
+ {
+ query_range:
+ 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^production-(.*)",namespace="autodevops-deploy-33"}) by (job)) without (job) /1024/1024/1024',
+ unit: 'GB',
+ label: 'Total',
+ result: [
+ {
+ metric: {},
+ values: [
+ [1558453960.079, '0.0357666015625'],
+ [1558454020.079, '0.035675048828125'],
+ [1558454080.079, '0.035152435302734375'],
+ [1558454140.079, '0.035221099853515625'],
+ [1558454200.079, '0.0352325439453125'],
+ [1558454260.079, '0.03479766845703125'],
+ [1558454320.079, '0.034793853759765625'],
+ [1558454380.079, '0.034931182861328125'],
+ [1558454440.079, '0.034816741943359375'],
+ [1558454500.079, '0.034816741943359375'],
+ [1558454560.079, '0.034816741943359375'],
+ ],
+ },
+ ],
+ },
+ ],
+ id: 15,
+ },
+ ],
+ },
+];
+
export default metricsGroupsAPIResponse;
export const deploymentData = [
@@ -733,5836 +779,6 @@ export const statePaths = {
documentationPath: '/help/administration/monitoring/prometheus/index.md',
};
-export const singleRowMetricsMultipleSeries = [
- {
- title: 'Multiple Time Series',
- weight: 1,
- y_label: 'Request Rates',
- queries: [
- {
- query_range:
- 'sum(rate(nginx_responses_total{environment="production"}[2m])) by (status_code)',
- label: 'Requests',
- unit: 'Req/sec',
- result: [
- {
- metric: {
- status_code: '1xx',
- },
- values: [
- {
- time: '2017-08-27T11:01:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:02:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:03:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:04:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:05:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:06:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:07:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:08:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:09:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:10:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:11:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:12:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:13:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:14:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:15:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:16:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:17:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:18:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:19:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:20:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:21:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:22:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:23:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:24:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:25:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:26:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:27:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:28:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:29:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:30:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:31:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:32:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:33:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:34:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:35:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:36:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:37:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:38:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:39:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:40:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:41:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:42:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:43:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:44:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:45:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:46:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:47:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:48:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:49:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:50:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:51:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:52:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:53:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:54:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:55:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:56:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:57:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:58:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T11:59:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:00:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:01:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:02:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:03:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:04:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:05:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:06:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:07:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:08:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:09:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:10:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:11:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:12:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:13:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:14:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:15:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:16:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:17:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:18:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:19:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:20:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:21:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:22:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:23:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:24:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:25:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:26:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:27:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:28:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:29:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:30:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:31:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:32:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:33:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:34:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:35:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:36:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:37:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:38:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:39:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:40:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:41:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:42:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:43:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:44:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:45:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:46:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:47:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:48:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:49:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:50:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:51:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:52:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:53:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:54:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:55:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:56:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:57:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:58:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T12:59:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:00:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:01:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:02:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:03:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:04:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:05:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:06:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:07:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:08:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:09:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:10:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:11:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:12:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:13:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:14:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:15:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:16:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:17:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:18:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:19:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:20:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:21:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:22:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:23:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:24:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:25:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:26:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:27:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:28:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:29:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:30:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:31:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:32:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:33:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:34:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:35:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:36:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:37:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:38:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:39:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:40:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:41:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:42:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:43:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:44:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:45:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:46:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:47:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:48:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:49:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:50:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:51:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:52:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:53:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:54:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:55:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:56:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:57:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:58:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T13:59:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:00:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:01:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:02:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:03:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:04:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:05:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:06:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:07:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:08:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:09:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:10:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:11:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:12:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:13:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:14:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:15:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:16:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:17:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:18:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:19:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:20:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:21:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:22:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:23:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:24:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:25:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:26:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:27:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:28:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:29:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:30:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:31:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:32:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:33:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:34:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:35:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:36:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:37:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:38:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:39:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:40:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:41:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:42:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:43:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:44:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:45:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:46:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:47:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:48:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:49:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:50:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:51:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:52:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:53:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:54:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:55:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:56:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:57:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:58:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T14:59:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:00:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:01:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:02:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:03:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:04:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:05:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:06:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:07:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:08:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:09:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:10:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:11:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:12:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:13:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:14:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:15:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:16:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:17:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:18:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:19:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:20:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:21:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:22:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:23:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:24:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:25:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:26:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:27:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:28:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:29:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:30:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:31:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:32:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:33:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:34:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:35:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:36:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:37:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:38:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:39:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:40:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:41:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:42:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:43:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:44:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:45:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:46:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:47:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:48:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:49:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:50:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:51:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:52:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:53:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:54:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:55:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:56:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:57:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:58:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T15:59:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:00:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:01:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:02:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:03:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:04:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:05:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:06:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:07:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:08:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:09:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:10:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:11:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:12:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:13:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:14:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:15:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:16:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:17:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:18:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:19:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:20:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:21:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:22:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:23:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:24:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:25:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:26:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:27:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:28:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:29:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:30:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:31:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:32:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:33:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:34:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:35:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:36:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:37:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:38:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:39:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:40:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:41:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:42:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:43:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:44:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:45:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:46:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:47:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:48:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:49:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:50:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:51:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:52:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:53:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:54:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:55:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:56:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:57:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:58:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T16:59:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:00:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:01:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:02:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:03:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:04:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:05:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:06:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:07:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:08:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:09:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:10:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:11:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:12:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:13:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:14:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:15:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:16:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:17:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:18:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:19:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:20:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:21:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:22:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:23:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:24:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:25:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:26:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:27:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:28:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:29:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:30:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:31:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:32:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:33:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:34:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:35:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:36:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:37:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:38:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:39:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:40:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:41:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:42:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:43:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:44:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:45:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:46:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:47:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:48:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:49:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:50:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:51:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:52:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:53:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:54:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:55:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:56:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:57:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:58:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T17:59:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:00:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:01:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:02:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:03:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:04:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:05:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:06:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:07:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:08:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:09:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:10:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:11:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:12:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:13:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:14:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:15:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:16:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:17:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:18:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:19:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:20:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:21:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:22:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:23:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:24:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:25:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:26:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:27:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:28:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:29:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:30:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:31:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:32:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:33:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:34:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:35:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:36:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:37:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:38:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:39:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:40:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:41:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:42:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:43:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:44:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:45:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:46:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:47:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:48:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:49:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:50:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:51:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:52:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:53:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:54:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:55:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:56:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:57:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:58:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T18:59:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T19:00:51.462Z',
- value: '0',
- },
- {
- time: '2017-08-27T19:01:51.462Z',
- value: '0',
- },
- ],
- },
- {
- metric: {
- status_code: '2xx',
- },
- values: [
- {
- time: '2017-08-27T11:01:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:02:51.462Z',
- value: '1.2571428571428571',
- },
- {
- time: '2017-08-27T11:03:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:04:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:05:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:06:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:07:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:08:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:09:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:10:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:11:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:12:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:13:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:14:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:15:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:16:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:17:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:18:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:19:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:20:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:21:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:22:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:23:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:24:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:25:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:26:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:27:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:28:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:29:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:30:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:31:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:32:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:33:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:34:51.462Z',
- value: '1.333320635041571',
- },
- {
- time: '2017-08-27T11:35:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:36:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:37:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:38:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:39:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:40:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:41:51.462Z',
- value: '1.3333587306424883',
- },
- {
- time: '2017-08-27T11:42:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:43:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:44:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:45:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:46:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:47:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:48:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:49:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T11:50:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:51:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:52:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:53:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:54:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:55:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:56:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:57:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T11:58:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T11:59:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:00:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:01:51.462Z',
- value: '1.3333460318669703',
- },
- {
- time: '2017-08-27T12:02:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:03:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:04:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:05:51.462Z',
- value: '1.31427319739812',
- },
- {
- time: '2017-08-27T12:06:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:07:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:08:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:09:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:10:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:11:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:12:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:13:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:14:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:15:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:16:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:17:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:18:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:19:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:20:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:21:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:22:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:23:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:24:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:25:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:26:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:27:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:28:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:29:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:30:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:31:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:32:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:33:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:34:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:35:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:36:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:37:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:38:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:39:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:40:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:41:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:42:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:43:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:44:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:45:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:46:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:47:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:48:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:49:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:50:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:51:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:52:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:53:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:54:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T12:55:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:56:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:57:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T12:58:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T12:59:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:00:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:01:51.462Z',
- value: '1.295225759754669',
- },
- {
- time: '2017-08-27T13:02:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:03:51.462Z',
- value: '1.2952627669098458',
- },
- {
- time: '2017-08-27T13:04:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:05:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:06:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:07:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:08:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:09:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:10:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:11:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:12:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:13:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:14:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:15:51.462Z',
- value: '1.2571428571428571',
- },
- {
- time: '2017-08-27T13:16:51.462Z',
- value: '1.3333587306424883',
- },
- {
- time: '2017-08-27T13:17:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:18:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:19:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:20:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:21:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:22:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:23:51.462Z',
- value: '1.276190476190476',
- },
- {
- time: '2017-08-27T13:24:51.462Z',
- value: '1.2571428571428571',
- },
- {
- time: '2017-08-27T13:25:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:26:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:27:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:28:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:29:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:30:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:31:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:32:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:33:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:34:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:35:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:36:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:37:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:38:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:39:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:40:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:41:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:42:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:43:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:44:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:45:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:46:51.462Z',
- value: '1.2571428571428571',
- },
- {
- time: '2017-08-27T13:47:51.462Z',
- value: '1.276190476190476',
- },
- {
- time: '2017-08-27T13:48:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:49:51.462Z',
- value: '1.295225759754669',
- },
- {
- time: '2017-08-27T13:50:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:51:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:52:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:53:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:54:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:55:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:56:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T13:57:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T13:58:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T13:59:51.462Z',
- value: '1.295225759754669',
- },
- {
- time: '2017-08-27T14:00:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:01:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:02:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:03:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:04:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:05:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:06:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:07:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:08:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:09:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:10:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:11:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:12:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:13:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:14:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:15:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:16:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:17:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:18:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:19:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:20:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:21:51.462Z',
- value: '1.3333079369916765',
- },
- {
- time: '2017-08-27T14:22:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:23:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:24:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:25:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:26:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:27:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:28:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:29:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:30:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:31:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:32:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:33:51.462Z',
- value: '1.2571428571428571',
- },
- {
- time: '2017-08-27T14:34:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:35:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:36:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:37:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:38:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:39:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:40:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:41:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:42:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:43:51.462Z',
- value: '1.276190476190476',
- },
- {
- time: '2017-08-27T14:44:51.462Z',
- value: '1.2571428571428571',
- },
- {
- time: '2017-08-27T14:45:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:46:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:47:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:48:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:49:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:50:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:51:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:52:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:53:51.462Z',
- value: '1.333320635041571',
- },
- {
- time: '2017-08-27T14:54:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:55:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T14:56:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:57:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T14:58:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T14:59:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T15:00:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:01:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:02:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:03:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:04:51.462Z',
- value: '1.2571428571428571',
- },
- {
- time: '2017-08-27T15:05:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:06:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:07:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:08:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:09:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:10:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:11:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:12:51.462Z',
- value: '1.31427319739812',
- },
- {
- time: '2017-08-27T15:13:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:14:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:15:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:16:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T15:17:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:18:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:19:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:20:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T15:21:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:22:51.462Z',
- value: '1.3333460318669703',
- },
- {
- time: '2017-08-27T15:23:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:24:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:25:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:26:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:27:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:28:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:29:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:30:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:31:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T15:32:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:33:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T15:34:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:35:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T15:36:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:37:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:38:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T15:39:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:40:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:41:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:42:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:43:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:44:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:45:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:46:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:47:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:48:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:49:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T15:50:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:51:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:52:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:53:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:54:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:55:51.462Z',
- value: '1.3333587306424883',
- },
- {
- time: '2017-08-27T15:56:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T15:57:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:58:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T15:59:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:00:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:01:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:02:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:03:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:04:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:05:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:06:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:07:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:08:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:09:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:10:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:11:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:12:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:13:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:14:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:15:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:16:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:17:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:18:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:19:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:20:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:21:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:22:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:23:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:24:51.462Z',
- value: '1.295225759754669',
- },
- {
- time: '2017-08-27T16:25:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:26:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:27:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:28:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:29:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:30:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:31:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:32:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:33:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:34:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:35:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:36:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:37:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:38:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:39:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:40:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:41:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:42:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:43:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:44:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:45:51.462Z',
- value: '1.3142982314117277',
- },
- {
- time: '2017-08-27T16:46:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:47:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:48:51.462Z',
- value: '1.333320635041571',
- },
- {
- time: '2017-08-27T16:49:51.462Z',
- value: '1.31427319739812',
- },
- {
- time: '2017-08-27T16:50:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:51:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:52:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:53:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:54:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:55:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T16:56:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:57:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T16:58:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T16:59:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:00:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:01:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:02:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:03:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:04:51.462Z',
- value: '1.2952504309564854',
- },
- {
- time: '2017-08-27T17:05:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:06:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:07:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:08:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:09:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:10:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:11:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:12:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:13:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:14:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:15:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:16:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:17:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:18:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:19:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:20:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:21:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:22:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:23:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:24:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:25:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:26:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:27:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:28:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:29:51.462Z',
- value: '1.295225759754669',
- },
- {
- time: '2017-08-27T17:30:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:31:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:32:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:33:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:34:51.462Z',
- value: '1.295225759754669',
- },
- {
- time: '2017-08-27T17:35:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:36:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:37:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:38:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:39:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:40:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:41:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:42:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:43:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:44:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:45:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:46:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:47:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:48:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:49:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:50:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:51:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:52:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:53:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:54:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:55:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T17:56:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:57:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T17:58:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T17:59:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T18:00:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:01:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:02:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:03:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:04:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:05:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:06:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:07:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:08:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:09:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:10:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:11:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:12:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T18:13:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:14:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:15:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:16:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:17:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:18:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:19:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:20:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:21:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:22:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:23:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:24:51.462Z',
- value: '1.2571428571428571',
- },
- {
- time: '2017-08-27T18:25:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:26:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:27:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:28:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:29:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:30:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:31:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:32:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:33:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:34:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:35:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:36:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:37:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T18:38:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:39:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:40:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:41:51.462Z',
- value: '1.580952380952381',
- },
- {
- time: '2017-08-27T18:42:51.462Z',
- value: '1.7333333333333334',
- },
- {
- time: '2017-08-27T18:43:51.462Z',
- value: '2.057142857142857',
- },
- {
- time: '2017-08-27T18:44:51.462Z',
- value: '2.1904761904761902',
- },
- {
- time: '2017-08-27T18:45:51.462Z',
- value: '1.8285714285714287',
- },
- {
- time: '2017-08-27T18:46:51.462Z',
- value: '2.1142857142857143',
- },
- {
- time: '2017-08-27T18:47:51.462Z',
- value: '1.619047619047619',
- },
- {
- time: '2017-08-27T18:48:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:49:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:50:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T18:51:51.462Z',
- value: '1.2952504309564854',
- },
- {
- time: '2017-08-27T18:52:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:53:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:54:51.462Z',
- value: '1.3333333333333333',
- },
- {
- time: '2017-08-27T18:55:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:56:51.462Z',
- value: '1.314285714285714',
- },
- {
- time: '2017-08-27T18:57:51.462Z',
- value: '1.295238095238095',
- },
- {
- time: '2017-08-27T18:58:51.462Z',
- value: '1.7142857142857142',
- },
- {
- time: '2017-08-27T18:59:51.462Z',
- value: '1.7333333333333334',
- },
- {
- time: '2017-08-27T19:00:51.462Z',
- value: '1.3904761904761904',
- },
- {
- time: '2017-08-27T19:01:51.462Z',
- value: '1.5047619047619047',
- },
- ],
- },
- ],
- when: [
- {
- value: 'hundred(s)',
- color: 'green',
- },
- ],
- },
- ],
- },
- {
- title: 'Throughput',
- weight: 1,
- y_label: 'Requests / Sec',
- queries: [
- {
- query_range:
- "sum(rate(nginx_requests_total{server_zone!='*', server_zone!='_', container_name!='POD',environment='production'}[2m]))",
- label: 'Total',
- unit: 'req / sec',
- result: [
- {
- metric: {},
- values: [
- {
- time: '2017-08-27T11:01:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:02:51.462Z',
- value: '0.45714285714285713',
- },
- {
- time: '2017-08-27T11:03:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:04:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:05:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:06:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:07:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:08:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:09:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:10:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:11:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:12:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:13:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:14:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:15:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:16:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:17:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:18:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:19:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:20:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:21:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:22:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:23:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:24:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:25:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:26:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:27:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:28:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:29:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:30:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:31:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:32:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:33:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:34:51.462Z',
- value: '0.4952333787297264',
- },
- {
- time: '2017-08-27T11:35:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:36:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:37:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:38:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:39:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:40:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:41:51.462Z',
- value: '0.49524752852435283',
- },
- {
- time: '2017-08-27T11:42:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:43:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:44:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:45:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:46:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:47:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:48:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:49:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T11:50:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:51:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:52:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:53:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:54:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:55:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:56:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:57:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T11:58:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T11:59:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:00:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:01:51.462Z',
- value: '0.49524281183630325',
- },
- {
- time: '2017-08-27T12:02:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:03:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:04:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:05:51.462Z',
- value: '0.4857096599080009',
- },
- {
- time: '2017-08-27T12:06:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:07:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:08:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:09:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:10:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:11:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:12:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:13:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:14:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:15:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:16:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:17:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:18:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:19:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:20:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:21:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:22:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:23:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:24:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:25:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:26:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:27:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:28:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:29:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:30:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:31:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:32:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:33:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:34:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:35:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:36:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:37:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:38:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:39:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:40:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:41:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:42:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:43:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:44:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:45:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:46:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:47:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:48:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:49:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:50:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:51:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:52:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:53:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:54:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T12:55:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:56:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:57:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T12:58:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T12:59:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:00:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:01:51.462Z',
- value: '0.4761859410862754',
- },
- {
- time: '2017-08-27T13:02:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:03:51.462Z',
- value: '0.4761995466580315',
- },
- {
- time: '2017-08-27T13:04:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:05:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:06:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:07:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:08:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:09:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:10:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:11:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:12:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:13:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:14:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:15:51.462Z',
- value: '0.45714285714285713',
- },
- {
- time: '2017-08-27T13:16:51.462Z',
- value: '0.49524752852435283',
- },
- {
- time: '2017-08-27T13:17:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:18:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:19:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:20:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:21:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:22:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:23:51.462Z',
- value: '0.4666666666666667',
- },
- {
- time: '2017-08-27T13:24:51.462Z',
- value: '0.45714285714285713',
- },
- {
- time: '2017-08-27T13:25:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:26:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:27:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:28:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:29:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:30:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:31:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:32:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:33:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:34:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:35:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:36:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:37:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:38:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:39:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:40:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:41:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:42:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:43:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:44:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:45:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:46:51.462Z',
- value: '0.45714285714285713',
- },
- {
- time: '2017-08-27T13:47:51.462Z',
- value: '0.4666666666666667',
- },
- {
- time: '2017-08-27T13:48:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:49:51.462Z',
- value: '0.4761859410862754',
- },
- {
- time: '2017-08-27T13:50:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:51:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:52:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:53:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:54:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:55:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:56:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T13:57:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T13:58:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T13:59:51.462Z',
- value: '0.4761859410862754',
- },
- {
- time: '2017-08-27T14:00:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:01:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:02:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:03:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:04:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:05:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:06:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:07:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:08:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:09:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:10:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:11:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:12:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:13:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:14:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:15:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:16:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:17:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:18:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:19:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:20:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:21:51.462Z',
- value: '0.4952286623111941',
- },
- {
- time: '2017-08-27T14:22:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:23:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:24:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:25:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:26:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:27:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:28:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:29:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:30:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:31:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:32:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:33:51.462Z',
- value: '0.45714285714285713',
- },
- {
- time: '2017-08-27T14:34:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:35:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:36:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:37:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:38:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:39:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:40:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:41:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:42:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:43:51.462Z',
- value: '0.4666666666666667',
- },
- {
- time: '2017-08-27T14:44:51.462Z',
- value: '0.45714285714285713',
- },
- {
- time: '2017-08-27T14:45:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:46:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:47:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:48:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:49:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:50:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:51:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:52:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:53:51.462Z',
- value: '0.4952333787297264',
- },
- {
- time: '2017-08-27T14:54:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:55:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T14:56:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:57:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T14:58:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T14:59:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T15:00:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:01:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:02:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:03:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:04:51.462Z',
- value: '0.45714285714285713',
- },
- {
- time: '2017-08-27T15:05:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:06:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:07:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:08:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:09:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:10:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:11:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:12:51.462Z',
- value: '0.4857096599080009',
- },
- {
- time: '2017-08-27T15:13:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:14:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:15:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:16:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T15:17:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:18:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:19:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:20:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T15:21:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:22:51.462Z',
- value: '0.49524281183630325',
- },
- {
- time: '2017-08-27T15:23:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:24:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:25:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:26:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:27:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:28:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:29:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:30:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:31:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T15:32:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:33:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T15:34:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:35:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T15:36:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:37:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:38:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T15:39:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:40:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:41:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:42:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:43:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:44:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:45:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:46:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:47:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:48:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:49:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T15:50:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:51:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:52:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:53:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:54:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:55:51.462Z',
- value: '0.49524752852435283',
- },
- {
- time: '2017-08-27T15:56:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T15:57:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:58:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T15:59:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:00:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:01:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:02:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:03:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:04:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:05:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:06:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:07:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:08:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:09:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:10:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:11:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:12:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:13:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:14:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:15:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:16:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:17:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:18:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:19:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:20:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:21:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:22:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:23:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:24:51.462Z',
- value: '0.4761859410862754',
- },
- {
- time: '2017-08-27T16:25:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:26:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:27:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:28:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:29:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:30:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:31:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:32:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:33:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:34:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:35:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:36:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:37:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:38:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:39:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:40:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:41:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:42:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:43:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:44:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:45:51.462Z',
- value: '0.485718911608682',
- },
- {
- time: '2017-08-27T16:46:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:47:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:48:51.462Z',
- value: '0.4952333787297264',
- },
- {
- time: '2017-08-27T16:49:51.462Z',
- value: '0.4857096599080009',
- },
- {
- time: '2017-08-27T16:50:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:51:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:52:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:53:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:54:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:55:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T16:56:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:57:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T16:58:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T16:59:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:00:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:01:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:02:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:03:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:04:51.462Z',
- value: '0.47619501138106085',
- },
- {
- time: '2017-08-27T17:05:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:06:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:07:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:08:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:09:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:10:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:11:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:12:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:13:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:14:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:15:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:16:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:17:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:18:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:19:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:20:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:21:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:22:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:23:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:24:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:25:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:26:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:27:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:28:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:29:51.462Z',
- value: '0.4761859410862754',
- },
- {
- time: '2017-08-27T17:30:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:31:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:32:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:33:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:34:51.462Z',
- value: '0.4761859410862754',
- },
- {
- time: '2017-08-27T17:35:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:36:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:37:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:38:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:39:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:40:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:41:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:42:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:43:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:44:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:45:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:46:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:47:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:48:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:49:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:50:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:51:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:52:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:53:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:54:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:55:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T17:56:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:57:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T17:58:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T17:59:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T18:00:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:01:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:02:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:03:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:04:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:05:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:06:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:07:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:08:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:09:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:10:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:11:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:12:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T18:13:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:14:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:15:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:16:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:17:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:18:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:19:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:20:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:21:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:22:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:23:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:24:51.462Z',
- value: '0.45714285714285713',
- },
- {
- time: '2017-08-27T18:25:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:26:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:27:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:28:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:29:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:30:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:31:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:32:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:33:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:34:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:35:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:36:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:37:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T18:38:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:39:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:40:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:41:51.462Z',
- value: '0.6190476190476191',
- },
- {
- time: '2017-08-27T18:42:51.462Z',
- value: '0.6952380952380952',
- },
- {
- time: '2017-08-27T18:43:51.462Z',
- value: '0.857142857142857',
- },
- {
- time: '2017-08-27T18:44:51.462Z',
- value: '0.9238095238095239',
- },
- {
- time: '2017-08-27T18:45:51.462Z',
- value: '0.7428571428571429',
- },
- {
- time: '2017-08-27T18:46:51.462Z',
- value: '0.8857142857142857',
- },
- {
- time: '2017-08-27T18:47:51.462Z',
- value: '0.638095238095238',
- },
- {
- time: '2017-08-27T18:48:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:49:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:50:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T18:51:51.462Z',
- value: '0.47619501138106085',
- },
- {
- time: '2017-08-27T18:52:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:53:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:54:51.462Z',
- value: '0.4952380952380952',
- },
- {
- time: '2017-08-27T18:55:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:56:51.462Z',
- value: '0.4857142857142857',
- },
- {
- time: '2017-08-27T18:57:51.462Z',
- value: '0.47619047619047616',
- },
- {
- time: '2017-08-27T18:58:51.462Z',
- value: '0.6857142857142856',
- },
- {
- time: '2017-08-27T18:59:51.462Z',
- value: '0.6952380952380952',
- },
- {
- time: '2017-08-27T19:00:51.462Z',
- value: '0.5238095238095237',
- },
- {
- time: '2017-08-27T19:01:51.462Z',
- value: '0.5904761904761905',
- },
- ],
- },
- ],
- },
- ],
- },
-];
-
export const queryWithoutData = {
title: 'HTTP Error rate',
weight: 10,
diff --git a/spec/javascripts/monitoring/monitoring_store_spec.js b/spec/javascripts/monitoring/monitoring_store_spec.js
deleted file mode 100644
index d8a980c874d..00000000000
--- a/spec/javascripts/monitoring/monitoring_store_spec.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import MonitoringStore from '~/monitoring/stores/monitoring_store';
-import MonitoringMock, { deploymentData, environmentData } from './mock_data';
-
-describe('MonitoringStore', () => {
- const store = new MonitoringStore();
- store.storeMetrics(MonitoringMock.data);
-
- it('contains two groups that contains, one of which has two queries sorted by priority', () => {
- expect(store.groups).toBeDefined();
- expect(store.groups.length).toEqual(2);
- expect(store.groups[0].metrics.length).toEqual(2);
- });
-
- it('gets the metrics count for every group', () => {
- expect(store.getMetricsCount()).toEqual(3);
- });
-
- it('contains deployment data', () => {
- store.storeDeploymentData(deploymentData);
-
- expect(store.deploymentData).toBeDefined();
- expect(store.deploymentData.length).toEqual(3);
- expect(typeof store.deploymentData[0]).toEqual('object');
- });
-
- it('only stores environment data that contains deployments', () => {
- store.storeEnvironmentsData(environmentData);
-
- expect(store.environmentsData.length).toEqual(2);
- });
-
- it('removes the data if all the values from a query are not defined', () => {
- expect(store.groups[1].metrics[0].queries[0].result.length).toEqual(0);
- });
-});
diff --git a/spec/javascripts/monitoring/store/actions_spec.js b/spec/javascripts/monitoring/store/actions_spec.js
new file mode 100644
index 00000000000..a848cd24fe3
--- /dev/null
+++ b/spec/javascripts/monitoring/store/actions_spec.js
@@ -0,0 +1,158 @@
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
+import store from '~/monitoring/stores';
+import * as types from '~/monitoring/stores/mutation_types';
+import {
+ fetchDeploymentsData,
+ fetchEnvironmentsData,
+ requestMetricsData,
+ setEndpoints,
+ setGettingStartedEmptyState,
+} from '~/monitoring/stores/actions';
+import storeState from '~/monitoring/stores/state';
+import testAction from 'spec/helpers/vuex_action_helper';
+import { resetStore } from '../helpers';
+import { deploymentData, environmentData } from '../mock_data';
+
+describe('Monitoring store actions', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ resetStore(store);
+ mock.restore();
+ });
+
+ describe('requestMetricsData', () => {
+ it('sets emptyState to loading', () => {
+ const commit = jasmine.createSpy();
+ const { state } = store;
+
+ requestMetricsData({ state, commit });
+
+ expect(commit).toHaveBeenCalledWith(types.REQUEST_METRICS_DATA);
+ });
+ });
+
+ describe('fetchDeploymentsData', () => {
+ it('commits RECEIVE_DEPLOYMENTS_DATA_SUCCESS on error', done => {
+ const dispatch = jasmine.createSpy();
+ const { state } = store;
+ state.deploymentEndpoint = '/success';
+
+ mock.onGet(state.deploymentEndpoint).reply(200, {
+ deployments: deploymentData,
+ });
+
+ fetchDeploymentsData({ state, dispatch })
+ .then(() => {
+ expect(dispatch).toHaveBeenCalledWith('receiveDeploymentsDataSuccess', deploymentData);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('commits RECEIVE_DEPLOYMENTS_DATA_FAILURE on error', done => {
+ const dispatch = jasmine.createSpy();
+ const { state } = store;
+ state.deploymentEndpoint = '/error';
+
+ mock.onGet(state.deploymentEndpoint).reply(500);
+
+ fetchDeploymentsData({ state, dispatch })
+ .then(() => {
+ expect(dispatch).toHaveBeenCalledWith('receiveDeploymentsDataFailure');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('fetchEnvironmentsData', () => {
+ it('commits RECEIVE_ENVIRONMENTS_DATA_SUCCESS on error', done => {
+ const dispatch = jasmine.createSpy();
+ const { state } = store;
+ state.environmentsEndpoint = '/success';
+
+ mock.onGet(state.environmentsEndpoint).reply(200, {
+ environments: environmentData,
+ });
+
+ fetchEnvironmentsData({ state, dispatch })
+ .then(() => {
+ expect(dispatch).toHaveBeenCalledWith('receiveEnvironmentsDataSuccess', environmentData);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('commits RECEIVE_ENVIRONMENTS_DATA_FAILURE on error', done => {
+ const dispatch = jasmine.createSpy();
+ const { state } = store;
+ state.environmentsEndpoint = '/error';
+
+ mock.onGet(state.environmentsEndpoint).reply(500);
+
+ fetchEnvironmentsData({ state, dispatch })
+ .then(() => {
+ expect(dispatch).toHaveBeenCalledWith('receiveEnvironmentsDataFailure');
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('Set endpoints', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = storeState();
+ });
+
+ it('should commit SET_ENDPOINTS mutation', done => {
+ testAction(
+ setEndpoints,
+ {
+ metricsEndpoint: 'additional_metrics.json',
+ deploymentsEndpoint: 'deployments.json',
+ environmentsEndpoint: 'deployments.json',
+ },
+ mockedState,
+ [
+ {
+ type: types.SET_ENDPOINTS,
+ payload: {
+ metricsEndpoint: 'additional_metrics.json',
+ deploymentsEndpoint: 'deployments.json',
+ environmentsEndpoint: 'deployments.json',
+ },
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('Set empty states', () => {
+ let mockedState;
+
+ beforeEach(() => {
+ mockedState = storeState();
+ });
+
+ it('should commit SET_METRICS_ENDPOINT mutation', done => {
+ testAction(
+ setGettingStartedEmptyState,
+ null,
+ mockedState,
+ [{ type: types.SET_GETTING_STARTED_EMPTY_STATE }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/store/mutations_spec.js b/spec/javascripts/monitoring/store/mutations_spec.js
new file mode 100644
index 00000000000..882ee1dec14
--- /dev/null
+++ b/spec/javascripts/monitoring/store/mutations_spec.js
@@ -0,0 +1,92 @@
+import mutations from '~/monitoring/stores/mutations';
+import * as types from '~/monitoring/stores/mutation_types';
+import state from '~/monitoring/stores/state';
+import { metricsGroupsAPIResponse, deploymentData } from '../mock_data';
+
+describe('Monitoring mutations', () => {
+ let stateCopy;
+
+ beforeEach(() => {
+ stateCopy = state();
+ });
+
+ describe(types.RECEIVE_METRICS_DATA_SUCCESS, () => {
+ beforeEach(() => {
+ stateCopy.groups = [];
+ const groups = metricsGroupsAPIResponse.data;
+
+ mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
+ });
+
+ it('normalizes values', () => {
+ const expectedTimestamp = '2017-05-25T08:22:34.925Z';
+ const expectedValue = 0.0010794445585559514;
+ const [timestamp, value] = stateCopy.groups[0].metrics[0].queries[0].result[0].values[0];
+
+ expect(timestamp).toEqual(expectedTimestamp);
+ expect(value).toEqual(expectedValue);
+ });
+
+ it('contains two groups that contains, one of which has two queries sorted by priority', () => {
+ expect(stateCopy.groups).toBeDefined();
+ expect(stateCopy.groups.length).toEqual(2);
+ expect(stateCopy.groups[0].metrics.length).toEqual(2);
+ });
+
+ it('assigns queries a metric id', () => {
+ expect(stateCopy.groups[1].metrics[0].queries[0].metricId).toEqual('100');
+ });
+
+ it('removes the data if all the values from a query are not defined', () => {
+ expect(stateCopy.groups[1].metrics[0].queries[0].result.length).toEqual(0);
+ });
+
+ it('assigns metric id of null if metric has no id', () => {
+ stateCopy.groups = [];
+ const groups = metricsGroupsAPIResponse.data;
+ const noId = groups.map(group => ({
+ ...group,
+ ...{
+ metrics: group.metrics.map(metric => {
+ const { id, ...metricWithoutId } = metric;
+
+ return metricWithoutId;
+ }),
+ },
+ }));
+
+ mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, noId);
+
+ stateCopy.groups.forEach(group => {
+ group.metrics.forEach(metric => {
+ expect(metric.queries.every(query => query.metricId === null)).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, () => {
+ it('stores the deployment data', () => {
+ stateCopy.deploymentData = [];
+ mutations[types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](stateCopy, deploymentData);
+
+ expect(stateCopy.deploymentData).toBeDefined();
+ expect(stateCopy.deploymentData.length).toEqual(3);
+ expect(typeof stateCopy.deploymentData[0]).toEqual('object');
+ });
+ });
+
+ describe('SET_ENDPOINTS', () => {
+ it('should set all the endpoints', () => {
+ mutations[types.SET_ENDPOINTS](stateCopy, {
+ metricsEndpoint: 'additional_metrics.json',
+ environmentsEndpoint: 'environments.json',
+ deploymentsEndpoint: 'deployments.json',
+ });
+
+ expect(stateCopy.metricsEndpoint).toEqual('additional_metrics.json');
+ expect(stateCopy.environmentsEndpoint).toEqual('environments.json');
+ expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/utils_spec.js b/spec/javascripts/monitoring/utils_spec.js
new file mode 100644
index 00000000000..e3c455d1686
--- /dev/null
+++ b/spec/javascripts/monitoring/utils_spec.js
@@ -0,0 +1,29 @@
+import { getTimeDiff } from '~/monitoring/utils';
+import { timeWindows } from '~/monitoring/constants';
+
+describe('getTimeDiff', () => {
+ it('defaults to an 8 hour (28800s) difference', () => {
+ const params = getTimeDiff();
+
+ expect(params.end - params.start).toEqual(28800);
+ });
+
+ it('accepts time window as an argument', () => {
+ const params = getTimeDiff(timeWindows.thirtyMinutes);
+
+ expect(params.end - params.start).not.toEqual(28800);
+ });
+
+ it('returns a value for every defined time window', () => {
+ const nonDefaultWindows = Object.keys(timeWindows).filter(window => window !== 'eightHours');
+
+ nonDefaultWindows.forEach(window => {
+ const params = getTimeDiff(timeWindows[window]);
+ const diff = params.end - params.start;
+
+ // Ensure we're not returning the default, 28800 (the # of seconds in 8 hrs)
+ expect(diff).not.toEqual(28800);
+ expect(typeof diff).toEqual('number');
+ });
+ });
+});
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 1d7b885e64f..4e3140ce4f1 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -3,7 +3,7 @@ import NewBranchForm from '~/new_branch_form';
describe('Branch', function() {
describe('create a new branch', function() {
- preloadFixtures('branches/new_branch.html.raw');
+ preloadFixtures('branches/new_branch.html');
function fillNameWith(value) {
$('.js-branch-name')
@@ -16,7 +16,7 @@ describe('Branch', function() {
}
beforeEach(function() {
- loadFixtures('branches/new_branch.html.raw');
+ loadFixtures('branches/new_branch.html');
$('form').on('submit', function(e) {
return e.preventDefault();
});
diff --git a/spec/javascripts/notes/components/discussion_filter_note_spec.js b/spec/javascripts/notes/components/discussion_filter_note_spec.js
new file mode 100644
index 00000000000..52d2e7ce947
--- /dev/null
+++ b/spec/javascripts/notes/components/discussion_filter_note_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import DiscussionFilterNote from '~/notes/components/discussion_filter_note.vue';
+import eventHub from '~/notes/event_hub';
+
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('DiscussionFilterNote component', () => {
+ let vm;
+
+ const createComponent = () => {
+ const Component = Vue.extend(DiscussionFilterNote);
+
+ return mountComponent(Component);
+ };
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('timelineContent', () => {
+ it('returns string containing instruction for switching feed type', () => {
+ expect(vm.timelineContent).toBe(
+ "You're only seeing <b>other activity</b> in the feed. To add a comment, switch to one of the following options.",
+ );
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('selectFilter', () => {
+ it('emits `dropdownSelect` event on `eventHub` with provided param', () => {
+ spyOn(eventHub, '$emit');
+
+ vm.selectFilter(1);
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('dropdownSelect', 1);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element', () => {
+ expect(vm.$el.classList.contains('discussion-filter-note')).toBe(true);
+ });
+
+ it('renders comment icon element', () => {
+ expect(vm.$el.querySelector('.timeline-icon svg use').getAttribute('xlink:href')).toContain(
+ 'comment',
+ );
+ });
+
+ it('renders filter information note', () => {
+ expect(vm.$el.querySelector('.timeline-content').innerText.trim()).toContain(
+ "You're only seeing other activity in the feed. To add a comment, switch to one of the following options.",
+ );
+ });
+
+ it('renders filter buttons', () => {
+ const buttonsContainerEl = vm.$el.querySelector('.discussion-filter-actions');
+
+ expect(buttonsContainerEl.querySelector('button:first-child').innerText.trim()).toContain(
+ 'Show all activity',
+ );
+
+ expect(buttonsContainerEl.querySelector('button:last-child').innerText.trim()).toContain(
+ 'Show comments only',
+ );
+ });
+
+ it('clicking `Show all activity` button calls `selectFilter("all")` method', () => {
+ const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:first-child');
+ spyOn(vm, 'selectFilter');
+
+ showAllBtn.dispatchEvent(new Event('click'));
+
+ expect(vm.selectFilter).toHaveBeenCalledWith(0);
+ });
+
+ it('clicking `Show comments only` button calls `selectFilter("comments")` method', () => {
+ const showAllBtn = vm.$el.querySelector('.discussion-filter-actions button:last-child');
+ spyOn(vm, 'selectFilter');
+
+ showAllBtn.dispatchEvent(new Event('click'));
+
+ expect(vm.selectFilter).toHaveBeenCalledWith(1);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/discussion_filter_spec.js b/spec/javascripts/notes/components/discussion_filter_spec.js
index 91dab58ba7f..1c366aee8e2 100644
--- a/spec/javascripts/notes/components/discussion_filter_spec.js
+++ b/spec/javascripts/notes/components/discussion_filter_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import createStore from '~/notes/stores';
import DiscussionFilter from '~/notes/components/discussion_filter.vue';
-import { DISCUSSION_FILTERS_DEFAULT_VALUE } from '~/notes/constants';
+import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { discussionFiltersMock, discussionMock } from '../mock_data';
@@ -54,14 +54,18 @@ describe('DiscussionFilter component', () => {
});
it('updates to the selected item', () => {
- const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
+ const filterItem = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
+ );
filterItem.click();
expect(vm.currentFilter.title).toEqual(filterItem.textContent.trim());
});
it('only updates when selected filter changes', () => {
- const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button');
+ const filterItem = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
+ );
spyOn(vm, 'filterDiscussion');
filterItem.click();
@@ -70,21 +74,27 @@ describe('DiscussionFilter component', () => {
});
it('disables commenting when "Show history only" filter is applied', () => {
- const filterItem = vm.$el.querySelector('.dropdown-menu li:last-child button');
+ const filterItem = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.HISTORY}"] button`,
+ );
filterItem.click();
expect(vm.$store.state.commentsDisabled).toBe(true);
});
it('enables commenting when "Show history only" filter is not applied', () => {
- const filterItem = vm.$el.querySelector('.dropdown-menu li:first-child button');
+ const filterItem = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"] button`,
+ );
filterItem.click();
expect(vm.$store.state.commentsDisabled).toBe(false);
});
it('renders a dropdown divider for the default filter', () => {
- const defaultFilter = vm.$el.querySelector('.dropdown-menu li:first-child');
+ const defaultFilter = vm.$el.querySelector(
+ `.dropdown-menu li[data-filter-type="${DISCUSSION_FILTER_TYPES.ALL}"]`,
+ );
expect(defaultFilter.lastChild.classList).toContain('dropdown-divider');
});
diff --git a/spec/javascripts/notes/components/note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js
index d604e90b529..1f2c07385a7 100644
--- a/spec/javascripts/notes/components/note_actions_spec.js
+++ b/spec/javascripts/notes/components/note_actions_spec.js
@@ -58,6 +58,7 @@ describe('noteActions', () => {
it('should render emoji link', () => {
expect(wrapper.find('.js-add-award').exists()).toBe(true);
+ expect(wrapper.find('.js-add-award').attributes('data-position')).toBe('right');
});
describe('actions dropdown', () => {
@@ -65,7 +66,7 @@ describe('noteActions', () => {
expect(wrapper.find('.js-note-edit').exists()).toBe(true);
});
- it('should be possible to report abuse to GitLab', () => {
+ it('should be possible to report abuse to admin', () => {
expect(wrapper.find(`a[href="${props.reportAbusePath}"]`).exists()).toBe(true);
});
@@ -128,87 +129,33 @@ describe('noteActions', () => {
});
});
- describe('with feature flag replyToIndividualNotes enabled', () => {
+ describe('for showReply = true', () => {
beforeEach(() => {
- gon.features = {
- replyToIndividualNotes: true,
- };
- });
-
- afterEach(() => {
- gon.features = {};
- });
-
- describe('for showReply = true', () => {
- beforeEach(() => {
- wrapper = shallowMountNoteActions({
- ...props,
- showReply: true,
- });
- });
-
- it('shows a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
-
- expect(replyButton.exists()).toBe(true);
+ wrapper = shallowMountNoteActions({
+ ...props,
+ showReply: true,
});
});
- describe('for showReply = false', () => {
- beforeEach(() => {
- wrapper = shallowMountNoteActions({
- ...props,
- showReply: false,
- });
- });
-
- it('does not show a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
+ it('shows a reply button', () => {
+ const replyButton = wrapper.find({ ref: 'replyButton' });
- expect(replyButton.exists()).toBe(false);
- });
+ expect(replyButton.exists()).toBe(true);
});
});
- describe('with feature flag replyToIndividualNotes disabled', () => {
+ describe('for showReply = false', () => {
beforeEach(() => {
- gon.features = {
- replyToIndividualNotes: false,
- };
- });
-
- afterEach(() => {
- gon.features = {};
- });
-
- describe('for showReply = true', () => {
- beforeEach(() => {
- wrapper = shallowMountNoteActions({
- ...props,
- showReply: true,
- });
- });
-
- it('does not show a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
-
- expect(replyButton.exists()).toBe(false);
+ wrapper = shallowMountNoteActions({
+ ...props,
+ showReply: false,
});
});
- describe('for showReply = false', () => {
- beforeEach(() => {
- wrapper = shallowMountNoteActions({
- ...props,
- showReply: false,
- });
- });
-
- it('does not show a reply button', () => {
- const replyButton = wrapper.find({ ref: 'replyButton' });
+ it('does not show a reply button', () => {
+ const replyButton = wrapper.find({ ref: 'replyButton' });
- expect(replyButton.exists()).toBe(false);
- });
+ expect(replyButton.exists()).toBe(false);
});
});
});
diff --git a/spec/javascripts/notes/components/note_form_spec.js b/spec/javascripts/notes/components/note_form_spec.js
index 5db20fd285f..b632ee6736d 100644
--- a/spec/javascripts/notes/components/note_form_spec.js
+++ b/spec/javascripts/notes/components/note_form_spec.js
@@ -1,16 +1,36 @@
-import Vue from 'vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import createStore from '~/notes/stores';
-import issueNoteForm from '~/notes/components/note_form.vue';
+import NoteForm from '~/notes/components/note_form.vue';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
-import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_note_form component', () => {
+ const dummyAutosaveKey = 'some-autosave-key';
+ const dummyDraft = 'dummy draft content';
+
let store;
- let vm;
+ let wrapper;
let props;
+ const createComponentWrapper = () => {
+ const localVue = createLocalVue();
+ return shallowMount(NoteForm, {
+ store,
+ propsData: props,
+ // see https://gitlab.com/gitlab-org/gitlab-ce/issues/56317 for the following
+ localVue,
+ sync: false,
+ });
+ };
+
beforeEach(() => {
- const Component = Vue.extend(issueNoteForm);
+ spyOnDependency(NoteForm, 'getDraft').and.callFake(key => {
+ if (key === dummyAutosaveKey) {
+ return dummyDraft;
+ }
+
+ return null;
+ });
store = createStore();
store.dispatch('setNoteableData', noteableDataMock);
@@ -21,27 +41,31 @@ describe('issue_note_form component', () => {
noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.',
noteId: '545',
};
-
- vm = new Component({
- store,
- propsData: props,
- }).$mount();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('noteHash', () => {
+ beforeEach(() => {
+ wrapper = createComponentWrapper();
+ });
+
it('returns note hash string based on `noteId`', () => {
- expect(vm.noteHash).toBe(`#note_${props.noteId}`);
+ expect(wrapper.vm.noteHash).toBe(`#note_${props.noteId}`);
});
it('return note hash as `#` when `noteId` is empty', done => {
- vm.noteId = '';
- Vue.nextTick()
+ wrapper.setProps({
+ ...props,
+ noteId: '',
+ });
+
+ wrapper.vm
+ .$nextTick()
.then(() => {
- expect(vm.noteHash).toBe('#');
+ expect(wrapper.vm.noteHash).toBe('#');
})
.then(done)
.catch(done.fail);
@@ -49,97 +73,196 @@ describe('issue_note_form component', () => {
});
describe('conflicts editing', () => {
+ beforeEach(() => {
+ wrapper = createComponentWrapper();
+ });
+
it('should show conflict message if note changes outside the component', done => {
- vm.isEditing = true;
- vm.noteBody = 'Foo';
+ wrapper.setProps({
+ ...props,
+ isEditing: true,
+ noteBody: 'Foo',
+ });
+
const message =
'This comment has changed since you started editing, please review the updated comment to ensure information is not lost.';
- Vue.nextTick(() => {
- expect(
- vm.$el
- .querySelector('.js-conflict-edit-warning')
- .textContent.replace(/\s+/g, ' ')
- .trim(),
- ).toEqual(message);
- done();
- });
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ const conflictWarning = wrapper.find('.js-conflict-edit-warning');
+
+ expect(conflictWarning.exists()).toBe(true);
+ expect(
+ conflictWarning
+ .text()
+ .replace(/\s+/g, ' ')
+ .trim(),
+ ).toBe(message);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
describe('form', () => {
+ beforeEach(() => {
+ wrapper = createComponentWrapper();
+ });
+
it('should render text area with placeholder', () => {
- expect(vm.$el.querySelector('textarea').getAttribute('placeholder')).toEqual(
+ const textarea = wrapper.find('textarea');
+
+ expect(textarea.attributes('placeholder')).toEqual(
'Write a comment or drag your files here…',
);
});
it('should link to markdown docs', () => {
const { markdownDocsPath } = notesDataMock;
+ const markdownField = wrapper.find(MarkdownField);
+ const markdownFieldProps = markdownField.props();
- expect(vm.$el.querySelector(`a[href="${markdownDocsPath}"]`).textContent.trim()).toEqual(
- 'Markdown',
- );
+ expect(markdownFieldProps.markdownDocsPath).toBe(markdownDocsPath);
});
describe('keyboard events', () => {
+ let textarea;
+
+ beforeEach(() => {
+ textarea = wrapper.find('textarea');
+ textarea.setValue('Foo');
+ });
+
describe('up', () => {
it('should ender edit mode', () => {
- spyOn(vm, 'editMyLastNote').and.callThrough();
- vm.$el.querySelector('textarea').value = 'Foo';
- vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(38, true));
+ // TODO: do not spy on vm
+ spyOn(wrapper.vm, 'editMyLastNote').and.callThrough();
- expect(vm.editMyLastNote).toHaveBeenCalled();
+ textarea.trigger('keydown.up');
+
+ expect(wrapper.vm.editMyLastNote).toHaveBeenCalled();
});
});
describe('enter', () => {
it('should save note when cmd+enter is pressed', () => {
- spyOn(vm, 'handleUpdate').and.callThrough();
- vm.$el.querySelector('textarea').value = 'Foo';
- vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, true));
+ textarea.trigger('keydown.enter', { metaKey: true });
+
+ const { handleFormUpdate } = wrapper.emitted();
- expect(vm.handleUpdate).toHaveBeenCalled();
+ expect(handleFormUpdate.length).toBe(1);
});
it('should save note when ctrl+enter is pressed', () => {
- spyOn(vm, 'handleUpdate').and.callThrough();
- vm.$el.querySelector('textarea').value = 'Foo';
- vm.$el.querySelector('textarea').dispatchEvent(keyboardDownEvent(13, false, true));
+ textarea.trigger('keydown.enter', { ctrlKey: true });
- expect(vm.handleUpdate).toHaveBeenCalled();
+ const { handleFormUpdate } = wrapper.emitted();
+
+ expect(handleFormUpdate.length).toBe(1);
});
});
});
describe('actions', () => {
it('should be possible to cancel', done => {
- spyOn(vm, 'cancelHandler').and.callThrough();
- vm.isEditing = true;
+ // TODO: do not spy on vm
+ spyOn(wrapper.vm, 'cancelHandler').and.callThrough();
+ wrapper.setProps({
+ ...props,
+ isEditing: true,
+ });
- Vue.nextTick(() => {
- vm.$el.querySelector('.note-edit-cancel').click();
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ const cancelButton = wrapper.find('.note-edit-cancel');
+ cancelButton.trigger('click');
- Vue.nextTick(() => {
- expect(vm.cancelHandler).toHaveBeenCalled();
- done();
- });
- });
+ expect(wrapper.vm.cancelHandler).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
});
it('should be possible to update the note', done => {
- vm.isEditing = true;
+ wrapper.setProps({
+ ...props,
+ isEditing: true,
+ });
- Vue.nextTick(() => {
- vm.$el.querySelector('textarea').value = 'Foo';
- vm.$el.querySelector('.js-vue-issue-save').click();
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ const textarea = wrapper.find('textarea');
+ textarea.setValue('Foo');
+ const saveButton = wrapper.find('.js-vue-issue-save');
+ saveButton.trigger('click');
- Vue.nextTick(() => {
- expect(vm.isSubmitting).toEqual(true);
- done();
- });
+ expect(wrapper.vm.isSubmitting).toEqual(true);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('with autosaveKey', () => {
+ describe('with draft', () => {
+ beforeEach(done => {
+ Object.assign(props, {
+ noteBody: '',
+ autosaveKey: dummyAutosaveKey,
});
+ wrapper = createComponentWrapper();
+
+ wrapper.vm
+ .$nextTick()
+ .then(done)
+ .catch(done.fail);
});
+
+ it('displays the draft in textarea', () => {
+ const textarea = wrapper.find('textarea');
+
+ expect(textarea.element.value).toBe(dummyDraft);
+ });
+ });
+
+ describe('without draft', () => {
+ beforeEach(done => {
+ Object.assign(props, {
+ noteBody: '',
+ autosaveKey: 'some key without draft',
+ });
+ wrapper = createComponentWrapper();
+
+ wrapper.vm
+ .$nextTick()
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('leaves the textarea empty', () => {
+ const textarea = wrapper.find('textarea');
+
+ expect(textarea.element.value).toBe('');
+ });
+ });
+
+ it('updates the draft if textarea content changes', () => {
+ const updateDraftSpy = spyOnDependency(NoteForm, 'updateDraft').and.stub();
+ Object.assign(props, {
+ noteBody: '',
+ autosaveKey: dummyAutosaveKey,
+ });
+ wrapper = createComponentWrapper();
+ const textarea = wrapper.find('textarea');
+ const dummyContent = 'some new content';
+
+ textarea.setValue(dummyContent);
+
+ expect(updateDraftSpy).toHaveBeenCalledWith(dummyAutosaveKey, dummyContent);
});
});
});
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 2b93fb9fb45..efa864e7d00 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -3,6 +3,7 @@ import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
+import NoteForm from '~/notes/components/note_form.vue';
import '~/behaviors/markdown/render_gfm';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
import mockDiffFile from '../../diffs/mock_data/diff_file';
@@ -72,7 +73,18 @@ describe('noteable_discussion component', () => {
.then(() => wrapper.vm.$nextTick())
.then(() => {
expect(wrapper.vm.isReplying).toEqual(true);
- expect(wrapper.vm.$refs.noteForm).not.toBeNull();
+
+ const noteForm = wrapper.find(NoteForm);
+
+ expect(noteForm.exists()).toBe(true);
+
+ const noteFormProps = noteForm.props();
+
+ expect(noteFormProps.discussion).toBe(discussionMock);
+ expect(noteFormProps.isEditing).toBe(false);
+ expect(noteFormProps.line).toBe(null);
+ expect(noteFormProps.saveButtonTitle).toBe('Comment');
+ expect(noteFormProps.autosaveKey).toBe(`Note/Issue/${discussionMock.id}/Reply`);
})
.then(done)
.catch(done.fail);
@@ -118,29 +130,6 @@ describe('noteable_discussion component', () => {
});
});
- describe('componentData', () => {
- it('should return first note object for placeholder note', () => {
- const data = {
- isPlaceholderNote: true,
- notes: [{ body: 'hello world!' }],
- };
-
- const note = wrapper.vm.componentData(data);
-
- expect(note).toEqual(data.notes[0]);
- });
-
- it('should return given note for nonplaceholder notes', () => {
- const data = {
- notes: [{ id: 12 }],
- };
-
- const note = wrapper.vm.componentData(data);
-
- expect(note).toEqual(data);
- });
- });
-
describe('action text', () => {
const commitId = 'razupaltuff';
const truncatedCommitId = commitId.substr(0, 8);
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 348743081eb..1df5cf9ef68 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -44,8 +44,7 @@ export const noteableDataMock = {
milestone: null,
milestone_id: null,
moved_to_id: null,
- preview_note_path:
- '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+ preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?target_id=98&target_type=Issue',
project_id: 2,
state: 'opened',
time_estimate: 0,
@@ -347,8 +346,7 @@ export const loggedOutnoteableData = {
},
noteable_note_url: '/group/project/merge_requests/1#note_1',
create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue',
- preview_note_path:
- '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+ preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?target_id=98&target_type=Issue',
};
export const collapseNotesMock = [
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 94ce6d8e222..7a9f32ddcff 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -3,11 +3,12 @@ import $ from 'jquery';
import _ from 'underscore';
import { TEST_HOST } from 'spec/test_constants';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
-import * as actions from '~/notes/stores/actions';
+import actionsModule, * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
+import service from '~/notes/services/notes_service';
import testAction from '../../helpers/vuex_action_helper';
import { resetStore } from '../helpers';
import {
@@ -18,11 +19,21 @@ import {
individualNote,
} from '../mock_data';
+const TEST_ERROR_MESSAGE = 'Test error message';
+
describe('Actions Notes Store', () => {
+ let commit;
+ let dispatch;
+ let state;
let store;
+ let flashSpy;
beforeEach(() => {
store = createStore();
+ commit = jasmine.createSpy('commit');
+ dispatch = jasmine.createSpy('dispatch');
+ state = {};
+ flashSpy = spyOnDependency(actionsModule, 'Flash');
});
afterEach(() => {
@@ -604,21 +615,6 @@ describe('Actions Notes Store', () => {
});
describe('updateOrCreateNotes', () => {
- let commit;
- let dispatch;
- let state;
-
- beforeEach(() => {
- commit = jasmine.createSpy('commit');
- dispatch = jasmine.createSpy('dispatch');
- state = {};
- });
-
- afterEach(() => {
- commit.calls.reset();
- dispatch.calls.reset();
- });
-
it('Updates existing note', () => {
const note = { id: 1234 };
const getters = { notesById: { 1234: note } };
@@ -751,4 +747,151 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('resolveDiscussion', () => {
+ let getters;
+ let discussionId;
+
+ beforeEach(() => {
+ discussionId = discussionMock.id;
+ state.discussions = [discussionMock];
+ getters = {
+ isDiscussionResolved: () => false,
+ };
+ });
+
+ it('when unresolved, dispatches action', done => {
+ testAction(
+ actions.resolveDiscussion,
+ { discussionId },
+ { ...state, ...getters },
+ [],
+ [
+ {
+ type: 'toggleResolveNote',
+ payload: {
+ endpoint: discussionMock.resolve_path,
+ isResolved: false,
+ discussion: true,
+ },
+ },
+ ],
+ done,
+ );
+ });
+
+ it('when resolved, does nothing', done => {
+ getters.isDiscussionResolved = id => id === discussionId;
+
+ testAction(
+ actions.resolveDiscussion,
+ { discussionId },
+ { ...state, ...getters },
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('saveNote', () => {
+ const payload = { endpoint: TEST_HOST, data: { 'note[note]': 'some text' } };
+
+ describe('if response contains errors', () => {
+ const res = { errors: { something: ['went wrong'] } };
+
+ it('throws an error', done => {
+ actions
+ .saveNote(
+ {
+ commit() {},
+ dispatch: () => Promise.resolve(res),
+ },
+ payload,
+ )
+ .then(() => done.fail('Expected error to be thrown!'))
+ .catch(error => {
+ expect(error.message).toBe('Failed to save comment!');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('if response contains no errors', () => {
+ const res = { valid: true };
+
+ it('returns the response', done => {
+ actions
+ .saveNote(
+ {
+ commit() {},
+ dispatch: () => Promise.resolve(res),
+ },
+ payload,
+ )
+ .then(data => {
+ expect(data).toBe(res);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('submitSuggestion', () => {
+ const discussionId = 'discussion-id';
+ const noteId = 'note-id';
+ const suggestionId = 'suggestion-id';
+ let flashContainer;
+
+ beforeEach(() => {
+ spyOn(service, 'applySuggestion');
+ dispatch.and.returnValue(Promise.resolve());
+ service.applySuggestion.and.returnValue(Promise.resolve());
+ flashContainer = {};
+ });
+
+ const testSubmitSuggestion = (done, expectFn) => {
+ actions
+ .submitSuggestion(
+ { commit, dispatch },
+ { discussionId, noteId, suggestionId, flashContainer },
+ )
+ .then(expectFn)
+ .then(done)
+ .catch(done.fail);
+ };
+
+ it('when service success, commits and resolves discussion', done => {
+ testSubmitSuggestion(done, () => {
+ expect(commit.calls.allArgs()).toEqual([
+ [mutationTypes.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }],
+ ]);
+
+ expect(dispatch.calls.allArgs()).toEqual([['resolveDiscussion', { discussionId }]]);
+ expect(flashSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ it('when service fails, flashes error message', done => {
+ const response = { response: { data: { message: TEST_ERROR_MESSAGE } } };
+
+ service.applySuggestion.and.returnValue(Promise.reject(response));
+
+ testSubmitSuggestion(done, () => {
+ expect(commit).not.toHaveBeenCalled();
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(flashSpy).toHaveBeenCalledWith(`${TEST_ERROR_MESSAGE}.`, 'alert', flashContainer);
+ });
+ });
+
+ it('when resolve discussion fails, fail gracefully', done => {
+ dispatch.and.returnValue(Promise.reject());
+
+ testSubmitSuggestion(done, () => {
+ expect(flashSpy).not.toHaveBeenCalled();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index c066975a43b..8f3c493dd4c 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -261,4 +261,12 @@ describe('Getters Notes Store', () => {
expect(getters.firstUnresolvedDiscussionId(state, localGettersFalsy)(false)).toBeFalsy();
});
});
+
+ describe('getDiscussion', () => {
+ it('returns discussion by ID', () => {
+ state.discussions.push({ id: '1' });
+
+ expect(getters.getDiscussion(state)('1')).toEqual({ id: '1' });
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index fcad1f245b6..4a640d589fb 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import mutations from '~/notes/stores/mutations';
+import { DISCUSSION_NOTE } from '~/notes/constants';
import {
note,
discussionMock,
@@ -326,6 +327,18 @@ describe('Notes Store mutations', () => {
expect(state.discussions[0].notes[0].note).toEqual('Foo');
});
+
+ it('transforms an individual note to discussion', () => {
+ const state = {
+ discussions: [individualNote],
+ };
+
+ const transformedNote = { ...individualNote.notes[0], type: DISCUSSION_NOTE };
+
+ mutations.UPDATE_NOTE(state, transformedNote);
+
+ expect(state.discussions[0].individual_note).toEqual(false);
+ });
});
describe('CLOSE_ISSUE', () => {
@@ -530,7 +543,7 @@ describe('Notes Store mutations', () => {
state = { convertedDisscussionIds: [] };
});
- it('adds a disucssion to convertedDisscussionIds', () => {
+ it('adds a discussion to convertedDisscussionIds', () => {
mutations.CONVERT_TO_DISCUSSION(state, discussion.id);
expect(state.convertedDisscussionIds).toContain(discussion.id);
@@ -549,7 +562,7 @@ describe('Notes Store mutations', () => {
state = { convertedDisscussionIds: [41, 42] };
});
- it('removes a disucssion from convertedDisscussionIds', () => {
+ it('removes a discussion from convertedDisscussionIds', () => {
mutations.REMOVE_CONVERTED_DISCUSSION(state, discussion.id);
expect(state.convertedDisscussionIds).not.toContain(discussion.id);
diff --git a/spec/javascripts/oauth_remember_me_spec.js b/spec/javascripts/oauth_remember_me_spec.js
index 4125706a407..381be82697e 100644
--- a/spec/javascripts/oauth_remember_me_spec.js
+++ b/spec/javascripts/oauth_remember_me_spec.js
@@ -2,10 +2,10 @@ import $ from 'jquery';
import OAuthRememberMe from '~/pages/sessions/new/oauth_remember_me';
describe('OAuthRememberMe', () => {
- preloadFixtures('static/oauth_remember_me.html.raw');
+ preloadFixtures('static/oauth_remember_me.html');
beforeEach(() => {
- loadFixtures('static/oauth_remember_me.html.raw');
+ loadFixtures('static/oauth_remember_me.html');
new OAuthRememberMe({ container: $('#oauth-container') }).bindEvents();
});
diff --git a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js b/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js
index 561bd2c96cb..6a239e307e9 100644
--- a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js
@@ -5,7 +5,7 @@ import initUserInternalRegexPlaceholder, {
} from '~/pages/admin/application_settings/account_and_limits';
describe('AccountAndLimits', () => {
- const FIXTURE = 'application_settings/accounts_and_limit.html.raw';
+ const FIXTURE = 'application_settings/accounts_and_limit.html';
let $userDefaultExternal;
let $userInternalRegex;
preloadFixtures(FIXTURE);
diff --git a/spec/javascripts/pages/admin/users/new/index_spec.js b/spec/javascripts/pages/admin/users/new/index_spec.js
index 5a849f34bc3..3896323eef7 100644
--- a/spec/javascripts/pages/admin/users/new/index_spec.js
+++ b/spec/javascripts/pages/admin/users/new/index_spec.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import UserInternalRegexHandler from '~/pages/admin/users/new/index';
describe('UserInternalRegexHandler', () => {
- const FIXTURE = 'admin/users/new_with_internal_user_regex.html.raw';
+ const FIXTURE = 'admin/users/new_with_internal_user_regex.html';
let $userExternal;
let $userEmail;
let $warningMessage;
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
new file mode 100644
index 00000000000..5f4dba5ecb9
--- /dev/null
+++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -0,0 +1,244 @@
+import $ from 'jquery';
+import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars
+import TimezoneDropdown, {
+ formatUtcOffset,
+ formatTimezone,
+ findTimezoneByIdentifier,
+} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
+
+describe('Timezone Dropdown', function() {
+ preloadFixtures('pipeline_schedules/edit.html');
+
+ let $inputEl = null;
+ let $dropdownEl = null;
+ let $wrapper = null;
+ const tzListSel = '.dropdown-content ul li a.is-active';
+ const tzDropdownToggleText = '.dropdown-toggle-text';
+
+ describe('Initialize', () => {
+ describe('with dropdown already loaded', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html');
+ $wrapper = $('.dropdown');
+ $inputEl = $('#schedule_cron_timezone');
+ $dropdownEl = $('.js-timezone-dropdown');
+
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ });
+ });
+
+ it('can take an $inputEl in the constructor', () => {
+ const tzStr = '[UTC + 5.5] Sri Jayawardenepura';
+ const tzValue = 'Asia/Colombo';
+
+ expect($inputEl.val()).toBe('UTC');
+
+ $(`${tzListSel}:contains('${tzStr}')`, $wrapper).trigger('click');
+
+ const val = $inputEl.val();
+
+ expect(val).toBe(tzValue);
+ expect(val).not.toBe('UTC');
+ });
+
+ it('will format data array of timezones into a list of offsets', () => {
+ const data = $dropdownEl.data('data');
+ const formatted = $wrapper.find(tzListSel).text();
+
+ data.forEach(item => {
+ expect(formatted).toContain(formatTimezone(item));
+ });
+ });
+
+ it('will default the timezone to UTC', () => {
+ const tz = $inputEl.val();
+
+ expect(tz).toBe('UTC');
+ });
+ });
+
+ describe('without dropdown loaded', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html');
+ $wrapper = $('.dropdown');
+ $inputEl = $('#schedule_cron_timezone');
+ $dropdownEl = $('.js-timezone-dropdown');
+ });
+
+ it('will populate the list of UTC offsets after the dropdown is loaded', () => {
+ expect($wrapper.find(tzListSel).length).toEqual(0);
+
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ });
+
+ expect($wrapper.find(tzListSel).length).toEqual($($dropdownEl).data('data').length);
+ });
+
+ it('will call a provided handler when a new timezone is selected', () => {
+ const onSelectTimezone = jasmine.createSpy('onSelectTimezoneMock');
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ onSelectTimezone,
+ });
+
+ $wrapper
+ .find(tzListSel)
+ .first()
+ .trigger('click');
+
+ expect(onSelectTimezone).toHaveBeenCalled();
+ });
+
+ it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => {
+ $inputEl.val('America/St_Johns');
+
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ displayFormat: selectedItem => formatTimezone(selectedItem),
+ });
+
+ expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland');
+ });
+
+ it('will call a provided `displayFormat` handler to format the dropdown value', () => {
+ const displayFormat = jasmine.createSpy('displayFormat');
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ displayFormat,
+ });
+
+ $wrapper
+ .find(tzListSel)
+ .first()
+ .trigger('click');
+
+ expect(displayFormat).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('formatUtcOffset', () => {
+ it('will convert negative utc offsets in seconds to hours and minutes', () => {
+ expect(formatUtcOffset(-21600)).toEqual('- 6');
+ });
+
+ it('will convert positive utc offsets in seconds to hours and minutes', () => {
+ expect(formatUtcOffset(25200)).toEqual('+ 7');
+ expect(formatUtcOffset(49500)).toEqual('+ 13.75');
+ });
+
+ it('will return 0 when given a string', () => {
+ expect(formatUtcOffset('BLAH')).toEqual('0');
+ expect(formatUtcOffset('$%$%')).toEqual('0');
+ });
+
+ it('will return 0 when given an array', () => {
+ expect(formatUtcOffset(['an', 'array'])).toEqual('0');
+ });
+
+ it('will return 0 when given an object', () => {
+ expect(formatUtcOffset({ some: '', object: '' })).toEqual('0');
+ });
+
+ it('will return 0 when given null', () => {
+ expect(formatUtcOffset(null)).toEqual('0');
+ });
+
+ it('will return 0 when given undefined', () => {
+ expect(formatUtcOffset(undefined)).toEqual('0');
+ });
+
+ it('will return 0 when given empty input', () => {
+ expect(formatUtcOffset('')).toEqual('0');
+ });
+ });
+
+ describe('formatTimezone', () => {
+ it('given name: "Chatham Is.", offset: "49500", will format for display as "[UTC + 13.75] Chatham Is."', () => {
+ expect(
+ formatTimezone({
+ name: 'Chatham Is.',
+ offset: 49500,
+ identifier: 'Pacific/Chatham',
+ }),
+ ).toEqual('[UTC + 13.75] Chatham Is.');
+ });
+
+ it('given name: "Saskatchewan", offset: "-21600", will format for display as "[UTC - 6] Saskatchewan"', () => {
+ expect(
+ formatTimezone({
+ name: 'Saskatchewan',
+ offset: -21600,
+ identifier: 'America/Regina',
+ }),
+ ).toEqual('[UTC - 6] Saskatchewan');
+ });
+
+ it('given name: "Accra", offset: "0", will format for display as "[UTC 0] Accra"', () => {
+ expect(
+ formatTimezone({
+ name: 'Accra',
+ offset: 0,
+ identifier: 'Africa/Accra',
+ }),
+ ).toEqual('[UTC 0] Accra');
+ });
+ });
+
+ describe('findTimezoneByIdentifier', () => {
+ const tzList = [
+ {
+ identifier: 'Asia/Tokyo',
+ name: 'Sapporo',
+ offset: 32400,
+ },
+ {
+ identifier: 'Asia/Hong_Kong',
+ name: 'Hong Kong',
+ offset: 28800,
+ },
+ {
+ identifier: 'Asia/Dhaka',
+ name: 'Dhaka',
+ offset: 21600,
+ },
+ ];
+
+ const identifier = 'Asia/Dhaka';
+ it('returns the correct object if the identifier exists', () => {
+ const res = findTimezoneByIdentifier(tzList, identifier);
+
+ expect(res).toBeTruthy();
+ expect(res).toBe(tzList[2]);
+ });
+
+ it('returns null if it doesnt find the identifier', () => {
+ const res = findTimezoneByIdentifier(tzList, 'Australia/Melbourne');
+
+ expect(res).toBeNull();
+ });
+
+ it('returns null if there is no identifier given', () => {
+ expect(findTimezoneByIdentifier(tzList)).toBeNull();
+ expect(findTimezoneByIdentifier(tzList, '')).toBeNull();
+ });
+
+ it('returns null if there is an empty or invalid array given', () => {
+ expect(findTimezoneByIdentifier([], identifier)).toBeNull();
+ expect(findTimezoneByIdentifier(null, identifier)).toBeNull();
+ expect(findTimezoneByIdentifier(undefined, identifier)).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js b/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js
index 7a8227479d4..1809e92e1d9 100644
--- a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js
+++ b/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js
@@ -2,10 +2,10 @@ import $ from 'jquery';
import preserveUrlFragment from '~/pages/sessions/new/preserve_url_fragment';
describe('preserve_url_fragment', () => {
- preloadFixtures('sessions/new.html.raw');
+ preloadFixtures('sessions/new.html');
beforeEach(() => {
- loadFixtures('sessions/new.html.raw');
+ loadFixtures('sessions/new.html');
});
it('adds the url fragment to all login and sign up form actions', () => {
diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js
index 699cf4871aa..c746d5644e8 100644
--- a/spec/javascripts/pdf/index_spec.js
+++ b/spec/javascripts/pdf/index_spec.js
@@ -1,11 +1,13 @@
import Vue from 'vue';
-import { PDFJS } from 'vendor/pdf';
-import workerSrc from 'vendor/pdf.worker.min';
+import { GlobalWorkerOptions } from 'pdfjs-dist/build/pdf';
+import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
import PDFLab from '~/pdf/index.vue';
-import pdf from '../fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
-PDFJS.workerSrc = workerSrc;
+const pdf = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
+
+GlobalWorkerOptions.workerSrc = workerSrc;
const Component = Vue.extend(PDFLab);
describe('PDF component', () => {
diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js
index ef967210b65..6dea570266b 100644
--- a/spec/javascripts/pdf/page_spec.js
+++ b/spec/javascripts/pdf/page_spec.js
@@ -1,10 +1,12 @@
import Vue from 'vue';
-import pdfjsLib from 'vendor/pdf';
-import workerSrc from 'vendor/pdf.worker.min';
+import pdfjsLib from 'pdfjs-dist/build/pdf';
+import workerSrc from 'pdfjs-dist/build/pdf.worker.min';
import PageComponent from '~/pdf/page/index.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import testPDF from 'spec/fixtures/blob/pdf/test.pdf';
+import { FIXTURES_PATH } from 'spec/test_constants';
+
+const testPDF = `${FIXTURES_PATH}/blob/pdf/test.pdf`;
describe('Page component', () => {
const Component = Vue.extend(PageComponent);
@@ -12,7 +14,7 @@ describe('Page component', () => {
let testPage;
beforeEach(done => {
- pdfjsLib.PDFJS.workerSrc = workerSrc;
+ pdfjsLib.GlobalWorkerOptions.workerSrc = workerSrc;
pdfjsLib
.getDocument(testPDF)
.then(pdf => pdf.getPage(1))
diff --git a/spec/javascripts/performance_bar/components/detailed_metric_spec.js b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
index e91685e50c5..8a7aa057186 100644
--- a/spec/javascripts/performance_bar/components/detailed_metric_spec.js
+++ b/spec/javascripts/performance_bar/components/detailed_metric_spec.js
@@ -27,8 +27,8 @@ describe('detailedMetric', () => {
describe('when the current request has details', () => {
const requestDetails = [
- { duration: '100', feature: 'find_commit', request: 'abcdef' },
- { duration: '23', feature: 'rebase_in_progress', request: '' },
+ { duration: '100', feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
+ { duration: '23', feature: 'rebase_in_progress', request: '', backtrace: ['world', 'hello'] },
];
beforeEach(() => {
@@ -54,9 +54,11 @@ describe('detailedMetric', () => {
});
it('adds a modal with a table of the details', () => {
- vm.$el.querySelectorAll('.performance-bar-modal td strong').forEach((duration, index) => {
- expect(duration.innerText).toContain(requestDetails[index].duration);
- });
+ vm.$el
+ .querySelectorAll('.performance-bar-modal td:nth-child(1)')
+ .forEach((duration, index) => {
+ expect(duration.innerText).toContain(requestDetails[index].duration);
+ });
vm.$el
.querySelectorAll('.performance-bar-modal td:nth-child(2)')
@@ -65,10 +67,16 @@ describe('detailedMetric', () => {
});
vm.$el
- .querySelectorAll('.performance-bar-modal td:nth-child(3)')
+ .querySelectorAll('.performance-bar-modal td:nth-child(2)')
.forEach((request, index) => {
- expect(request.innerText).toEqual(requestDetails[index].request);
+ expect(request.innerText).toContain(requestDetails[index].request);
});
+
+ expect(vm.$el.querySelector('.text-expander.js-toggle-button')).not.toBeNull();
+
+ vm.$el.querySelectorAll('.performance-bar-modal td:nth-child(2)').forEach(request => {
+ expect(request.innerText).toContain('world');
+ });
});
it('displays the metric name', () => {
diff --git a/spec/javascripts/persistent_user_callout_spec.js b/spec/javascripts/persistent_user_callout_spec.js
new file mode 100644
index 00000000000..2fdfff3db03
--- /dev/null
+++ b/spec/javascripts/persistent_user_callout_spec.js
@@ -0,0 +1,88 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import PersistentUserCallout from '~/persistent_user_callout';
+import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
+
+describe('PersistentUserCallout', () => {
+ const dismissEndpoint = '/dismiss';
+ const featureName = 'feature';
+
+ function createFixture() {
+ const fixture = document.createElement('div');
+ fixture.innerHTML = `
+ <div
+ class="container"
+ data-dismiss-endpoint="${dismissEndpoint}"
+ data-feature-id="${featureName}"
+ >
+ <button type="button" class="js-close"></button>
+ </div>
+ `;
+
+ return fixture;
+ }
+
+ describe('dismiss', () => {
+ let button;
+ let mockAxios;
+ let persistentUserCallout;
+
+ beforeEach(() => {
+ const fixture = createFixture();
+ const container = fixture.querySelector('.container');
+ button = fixture.querySelector('.js-close');
+ mockAxios = new MockAdapter(axios);
+ persistentUserCallout = new PersistentUserCallout(container);
+ spyOn(persistentUserCallout.container, 'remove');
+ });
+
+ afterEach(() => {
+ mockAxios.restore();
+ });
+
+ it('POSTs endpoint and removes container when clicking close', done => {
+ mockAxios.onPost(dismissEndpoint).replyOnce(200);
+
+ button.click();
+
+ setTimeoutPromise()
+ .then(() => {
+ expect(persistentUserCallout.container.remove).toHaveBeenCalled();
+ expect(mockAxios.history.post[0].data).toBe(
+ JSON.stringify({ feature_name: featureName }),
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('invokes Flash when the dismiss request fails', done => {
+ const Flash = spyOnDependency(PersistentUserCallout, 'Flash');
+ mockAxios.onPost(dismissEndpoint).replyOnce(500);
+
+ button.click();
+
+ setTimeoutPromise()
+ .then(() => {
+ expect(persistentUserCallout.container.remove).not.toHaveBeenCalled();
+ expect(Flash).toHaveBeenCalledWith(
+ 'An error occurred while dismissing the alert. Refresh the page and try again.',
+ );
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('factory', () => {
+ it('returns an instance of PersistentUserCallout with the provided container property', () => {
+ const fixture = createFixture();
+
+ expect(PersistentUserCallout.factory(fixture) instanceof PersistentUserCallout).toBe(true);
+ });
+
+ it('returns undefined if container is falsey', () => {
+ expect(PersistentUserCallout.factory()).toBe(undefined);
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/graph/action_component_spec.js b/spec/javascripts/pipelines/graph/action_component_spec.js
index 3d2232ff239..321497b35b5 100644
--- a/spec/javascripts/pipelines/graph/action_component_spec.js
+++ b/spec/javascripts/pipelines/graph/action_component_spec.js
@@ -55,13 +55,27 @@ describe('pipeline graph action component', () => {
component.$el.click();
- component
- .$nextTick()
- .then(() => {
- expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
- })
- .then(done)
- .catch(done.fail);
+ setTimeout(() => {
+ component
+ .$nextTick()
+ .then(() => {
+ expect(component.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
+ })
+ .catch(done.fail);
+
+ done();
+ }, 0);
+ });
+
+ it('renders a loading icon while waiting for request', done => {
+ component.$el.click();
+
+ component.$nextTick(() => {
+ expect(component.$el.querySelector('.js-action-icon-loading')).not.toBeNull();
+ setTimeout(() => {
+ done();
+ });
+ });
});
});
});
diff --git a/spec/javascripts/pipelines/graph/stage_column_component_spec.js b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
index d0b8f877d6f..5183f8dd2d6 100644
--- a/spec/javascripts/pipelines/graph/stage_column_component_spec.js
+++ b/spec/javascripts/pipelines/graph/stage_column_component_spec.js
@@ -35,6 +35,7 @@ describe('stage column component', () => {
component = mountComponent(StageColumnComponent, {
title: 'foo',
groups: mockGroups,
+ hasTriggeredBy: false,
});
});
@@ -54,13 +55,14 @@ describe('stage column component', () => {
id: 4259,
name: '<img src=x onerror=alert(document.domain)>',
status: {
- icon: 'icon_status_success',
+ icon: 'status_success',
label: 'success',
tooltip: '<img src=x onerror=alert(document.domain)>',
},
},
],
title: 'test',
+ hasTriggeredBy: false,
});
expect(component.$el.querySelector('.builds-container li').getAttribute('id')).toEqual(
@@ -68,4 +70,53 @@ describe('stage column component', () => {
);
});
});
+
+ describe('with action', () => {
+ it('renders action button', () => {
+ component = mountComponent(StageColumnComponent, {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
+ },
+ ],
+ title: 'test',
+ hasTriggeredBy: false,
+ action: {
+ icon: 'play',
+ title: 'Play all',
+ path: 'action',
+ },
+ });
+
+ expect(component.$el.querySelector('.js-stage-action')).not.toBeNull();
+ });
+ });
+
+ describe('without action', () => {
+ it('does not render action button', () => {
+ component = mountComponent(StageColumnComponent, {
+ groups: [
+ {
+ id: 4259,
+ name: '<img src=x onerror=alert(document.domain)>',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: '<img src=x onerror=alert(document.domain)>',
+ },
+ },
+ ],
+ title: 'test',
+ hasTriggeredBy: false,
+ });
+
+ expect(component.$el.querySelector('.js-stage-action')).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/mock_data.js b/spec/javascripts/pipelines/mock_data.js
index 03ead6cd8ba..8eef9166b8d 100644
--- a/spec/javascripts/pipelines/mock_data.js
+++ b/spec/javascripts/pipelines/mock_data.js
@@ -1,5 +1,6 @@
export const pipelineWithStages = {
id: 20333396,
+ iid: 304399,
user: {
id: 128633,
name: 'Rémy Coutable',
diff --git a/spec/javascripts/pipelines/pipeline_triggerer_spec.js b/spec/javascripts/pipelines/pipeline_triggerer_spec.js
new file mode 100644
index 00000000000..8cf290f2663
--- /dev/null
+++ b/spec/javascripts/pipelines/pipeline_triggerer_spec.js
@@ -0,0 +1,54 @@
+import { mount } from '@vue/test-utils';
+import pipelineTriggerer from '~/pipelines/components/pipeline_triggerer.vue';
+
+describe('Pipelines Triggerer', () => {
+ let wrapper;
+
+ const mockData = {
+ pipeline: {
+ user: {
+ name: 'foo',
+ avatar_url: '/avatar',
+ path: '/path',
+ },
+ },
+ };
+
+ const createComponent = () => {
+ wrapper = mount(pipelineTriggerer, {
+ propsData: mockData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render a table cell', () => {
+ expect(wrapper.contains('.table-section')).toBe(true);
+ });
+
+ it('should render triggerer information when triggerer is provided', () => {
+ const link = wrapper.find('.js-pipeline-url-user');
+
+ expect(link.attributes('href')).toEqual(mockData.pipeline.user.path);
+ expect(link.find('.js-user-avatar-image-toolip').text()).toEqual(mockData.pipeline.user.name);
+ expect(link.find('img.avatar').attributes('src')).toEqual(
+ `${mockData.pipeline.user.avatar_url}?width=26`,
+ );
+ });
+
+ it('should render "API" when no triggerer is provided', () => {
+ wrapper.setProps({
+ pipeline: {
+ user: null,
+ },
+ });
+
+ expect(wrapper.find('.js-pipeline-url-api').text()).toEqual('API');
+ });
+});
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index ea917b36526..88c0137dc58 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -13,6 +13,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {},
},
@@ -28,6 +29,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {},
},
@@ -42,65 +44,19 @@ describe('Pipeline Url Component', () => {
expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1');
});
- it('should render user information when a user is provided', () => {
- const mockData = {
- pipeline: {
- id: 1,
- path: 'foo',
- flags: {},
- user: {
- web_url: '/',
- name: 'foo',
- avatar_url: '/',
- path: '/',
- },
- },
- autoDevopsHelpPath: 'foo',
- };
-
- const component = new PipelineUrlComponent({
- propsData: mockData,
- }).$mount();
-
- const image = component.$el.querySelector('.js-pipeline-url-user img');
- const tooltip = component.$el.querySelector(
- '.js-pipeline-url-user .js-user-avatar-image-toolip',
- );
-
- expect(component.$el.querySelector('.js-pipeline-url-user').getAttribute('href')).toEqual(
- mockData.pipeline.user.web_url,
- );
-
- expect(tooltip.textContent.trim()).toEqual(mockData.pipeline.user.name);
- expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`);
- });
-
- it('should render "API" when no user is provided', () => {
- const component = new PipelineUrlComponent({
- propsData: {
- pipeline: {
- id: 1,
- path: 'foo',
- flags: {},
- },
- autoDevopsHelpPath: 'foo',
- },
- }).$mount();
-
- expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
- });
-
it('should render latest, yaml invalid, merge request, and stuck flags when provided', () => {
const component = new PipelineUrlComponent({
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
latest: true,
yaml_errors: true,
stuck: true,
- merge_request: true,
+ merge_request_pipeline: true,
+ detached_merge_request_pipeline: true,
},
},
autoDevopsHelpPath: 'foo',
@@ -108,15 +64,16 @@ describe('Pipeline Url Component', () => {
}).$mount();
expect(component.$el.querySelector('.js-pipeline-url-latest').textContent).toContain('latest');
+
expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain(
'yaml invalid',
);
- expect(component.$el.querySelector('.js-pipeline-url-mergerequest').textContent).toContain(
- 'merge request',
- );
-
expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
+
+ expect(component.$el.querySelector('.js-pipeline-url-detached').textContent).toContain(
+ 'detached',
+ );
});
it('should render a badge for autodevops', () => {
@@ -124,6 +81,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
latest: true,
@@ -146,6 +104,7 @@ describe('Pipeline Url Component', () => {
propsData: {
pipeline: {
id: 1,
+ iid: 1,
path: 'foo',
flags: {
failure_reason: true,
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index 97ded16db69..78187b69563 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -446,7 +446,7 @@ describe('Pipelines', () => {
};
vm.$nextTick(() => {
- vm.$el.querySelector('.js-next-button a').click();
+ vm.$el.querySelector('.js-next-button .page-link').click();
expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 4c575536f0e..d47504d2f54 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -80,13 +80,13 @@ describe('Pipelines Table Row', () => {
it('should render user information', () => {
expect(
component.$el
- .querySelector('.table-section:nth-child(2) a:nth-child(3)')
+ .querySelector('.table-section:nth-child(3) .js-pipeline-url-user')
.getAttribute('href'),
).toEqual(pipeline.user.path);
expect(
component.$el
- .querySelector('.table-section:nth-child(2) .js-user-avatar-image-toolip')
+ .querySelector('.table-section:nth-child(3) .js-user-avatar-image-toolip')
.textContent.trim(),
).toEqual(pipeline.user.name);
});
@@ -195,8 +195,10 @@ describe('Pipelines Table Row', () => {
it('emits `openConfirmationModal` event when cancel button is clicked and toggles loading', () => {
eventHub.$once('openConfirmationModal', data => {
+ const { id, ref, commit } = pipeline;
+
expect(data.endpoint).toEqual('/cancel');
- expect(data.pipelineId).toEqual(pipeline.id);
+ expect(data.pipeline).toEqual(jasmine.objectContaining({ id, ref, commit }));
});
component.$el.querySelector('.js-pipelines-cancel-button').click();
diff --git a/spec/javascripts/pipelines/stage_spec.js b/spec/javascripts/pipelines/stage_spec.js
index 3c8b8032de8..19ae7860333 100644
--- a/spec/javascripts/pipelines/stage_spec.js
+++ b/spec/javascripts/pipelines/stage_spec.js
@@ -120,13 +120,15 @@ describe('Pipelines stage component', () => {
setTimeout(() => {
component.$el.querySelector('.js-ci-action').click();
- component
- .$nextTick()
- .then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
- })
- .then(done)
- .catch(done.fail);
+ setTimeout(() => {
+ component
+ .$nextTick()
+ .then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ })
+ .then(done)
+ .catch(done.fail);
+ }, 0);
}, 0);
});
});
diff --git a/spec/javascripts/pipelines_spec.js b/spec/javascripts/pipelines_spec.js
index 6b86f9ea437..6d4d634c575 100644
--- a/spec/javascripts/pipelines_spec.js
+++ b/spec/javascripts/pipelines_spec.js
@@ -1,10 +1,10 @@
import Pipelines from '~/pipelines';
describe('Pipelines', () => {
- preloadFixtures('static/pipeline_graph.html.raw');
+ preloadFixtures('static/pipeline_graph.html');
beforeEach(() => {
- loadFixtures('static/pipeline_graph.html.raw');
+ loadFixtures('static/pipeline_graph.html');
});
it('should be defined', () => {
diff --git a/spec/javascripts/project_select_combo_button_spec.js b/spec/javascripts/project_select_combo_button_spec.js
index 109a5000f5d..dc85292c23e 100644
--- a/spec/javascripts/project_select_combo_button_spec.js
+++ b/spec/javascripts/project_select_combo_button_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import ProjectSelectComboButton from '~/project_select_combo_button';
-const fixturePath = 'static/project_select_combo_button.html.raw';
+const fixturePath = 'static/project_select_combo_button.html';
describe('Project Select Combo Button', function() {
preloadFixtures(fixturePath);
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js
index 030662b4d90..1eb7cb4bd5b 100644
--- a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js
@@ -53,36 +53,32 @@ describe('GkeProjectIdDropdown', () => {
});
it('returns default toggle text', done =>
- vm
- .$nextTick()
- .then(() => {
- vm.setItem(emptyProjectMock);
+ setTimeout(() => {
+ vm.setItem(emptyProjectMock);
- expect(vm.toggleText).toBe(LABELS.DEFAULT);
- done();
- })
- .catch(done.fail));
+ expect(vm.toggleText).toBe(LABELS.DEFAULT);
+
+ done();
+ }));
it('returns project name if project selected', done =>
- vm
- .$nextTick()
- .then(() => {
- expect(vm.toggleText).toBe(selectedProjectMock.name);
- done();
- })
- .catch(done.fail));
+ setTimeout(() => {
+ vm.isLoading = false;
+
+ expect(vm.toggleText).toBe(selectedProjectMock.name);
+
+ done();
+ }));
it('returns empty toggle text', done =>
- vm
- .$nextTick()
- .then(() => {
- vm.$store.commit(SET_PROJECTS, null);
- vm.setItem(emptyProjectMock);
+ setTimeout(() => {
+ vm.$store.commit(SET_PROJECTS, null);
+ vm.setItem(emptyProjectMock);
- expect(vm.toggleText).toBe(LABELS.EMPTY);
- done();
- })
- .catch(done.fail));
+ expect(vm.toggleText).toBe(LABELS.EMPTY);
+
+ done();
+ }));
});
describe('selectItem', () => {
diff --git a/spec/javascripts/projects/project_new_spec.js b/spec/javascripts/projects/project_new_spec.js
index b61e0ac872f..106a3ba94e4 100644
--- a/spec/javascripts/projects/project_new_spec.js
+++ b/spec/javascripts/projects/project_new_spec.js
@@ -10,7 +10,17 @@ describe('New Project', () => {
setFixtures(`
<div class='toggle-import-form'>
<div class='import-url-data'>
- <input id="project_import_url" />
+ <div class="form-group">
+ <input id="project_import_url" />
+ </div>
+ <div id="import-url-auth-method">
+ <div class="form-group">
+ <input id="project-import-url-user" />
+ </div>
+ <div class="form-group">
+ <input id="project_import_url_password" />
+ </div>
+ </div>
<input id="project_name" />
<input id="project_path" />
</div>
@@ -119,7 +129,7 @@ describe('New Project', () => {
});
it('changes project path for HTTPS URL in $projectImportUrl', () => {
- $projectImportUrl.val('https://username:password@gitlab.company.com/group/project.git');
+ $projectImportUrl.val('https://gitlab.company.com/group/project.git');
projectNew.deriveProjectPathFromUrl($projectImportUrl);
diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
index 94e2f959d46..dca3e1553b9 100644
--- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
@@ -5,7 +5,7 @@ import PANEL_STATE from '~/prometheus_metrics/constants';
import { metrics, missingVarMetrics } from './mock_data';
describe('PrometheusMetrics', () => {
- const FIXTURE = 'services/prometheus/prometheus_service.html.raw';
+ const FIXTURE = 'services/prometheus/prometheus_service.html';
preloadFixtures(FIXTURE);
beforeEach(() => {
diff --git a/spec/javascripts/raven/index_spec.js b/spec/javascripts/raven/index_spec.js
index a503a54029f..6b9fe923624 100644
--- a/spec/javascripts/raven/index_spec.js
+++ b/spec/javascripts/raven/index_spec.js
@@ -5,19 +5,19 @@ describe('RavenConfig options', () => {
const sentryDsn = 'sentryDsn';
const currentUserId = 'currentUserId';
const gitlabUrl = 'gitlabUrl';
- const isProduction = 'isProduction';
+ const environment = 'test';
const revision = 'revision';
let indexReturnValue;
beforeEach(() => {
window.gon = {
sentry_dsn: sentryDsn,
+ sentry_environment: environment,
current_user_id: currentUserId,
gitlab_url: gitlabUrl,
revision,
};
- process.env.NODE_ENV = isProduction;
process.env.HEAD_COMMIT_SHA = revision;
spyOn(RavenConfig, 'init');
@@ -25,12 +25,12 @@ describe('RavenConfig options', () => {
indexReturnValue = index();
});
- it('should init with .sentryDsn, .currentUserId, .whitelistUrls and .isProduction', () => {
+ it('should init with .sentryDsn, .currentUserId, .whitelistUrls and environment', () => {
expect(RavenConfig.init).toHaveBeenCalledWith({
sentryDsn,
currentUserId,
- whitelistUrls: [gitlabUrl],
- isProduction,
+ whitelistUrls: [gitlabUrl, 'webpack-internal://'],
+ environment,
release: revision,
tags: {
revision,
diff --git a/spec/javascripts/raven/raven_config_spec.js b/spec/javascripts/raven/raven_config_spec.js
index 5cc59cc28d3..af634a0c196 100644
--- a/spec/javascripts/raven/raven_config_spec.js
+++ b/spec/javascripts/raven/raven_config_spec.js
@@ -69,8 +69,8 @@ describe('RavenConfig', () => {
let ravenConfig;
const options = {
sentryDsn: '//sentryDsn',
- whitelistUrls: ['//gitlabUrl'],
- isProduction: true,
+ whitelistUrls: ['//gitlabUrl', 'webpack-internal://'],
+ environment: 'test',
release: 'revision',
tags: {
revision: 'revision',
@@ -95,7 +95,7 @@ describe('RavenConfig', () => {
release: options.release,
tags: options.tags,
whitelistUrls: options.whitelistUrls,
- environment: 'production',
+ environment: 'test',
ignoreErrors: ravenConfig.IGNORE_ERRORS,
ignoreUrls: ravenConfig.IGNORE_URLS,
shouldSendCallback: jasmine.any(Function),
@@ -106,8 +106,8 @@ describe('RavenConfig', () => {
expect(raven.install).toHaveBeenCalled();
});
- it('should set .environment to development if isProduction is false', () => {
- ravenConfig.options.isProduction = false;
+ it('should set environment from options', () => {
+ ravenConfig.options.environment = 'development';
RavenConfig.configure.call(ravenConfig);
diff --git a/spec/javascripts/read_more_spec.js b/spec/javascripts/read_more_spec.js
index b1af0f80a50..d1d01272403 100644
--- a/spec/javascripts/read_more_spec.js
+++ b/spec/javascripts/read_more_spec.js
@@ -1,7 +1,7 @@
import initReadMore from '~/read_more';
describe('Read more click-to-expand functionality', () => {
- const fixtureName = 'projects/overview.html.raw';
+ const fixtureName = 'projects/overview.html';
preloadFixtures(fixtureName);
diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js
index 67118ac03a5..76a17e6fb31 100644
--- a/spec/javascripts/registry/components/app_spec.js
+++ b/spec/javascripts/registry/components/app_spec.js
@@ -99,7 +99,7 @@ describe('Registry List', () => {
it('should render a loading spinner', done => {
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.fa-spinner')).not.toBe(null);
+ expect(vm.$el.querySelector('.spinner')).not.toBe(null);
done();
});
});
diff --git a/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js
new file mode 100644
index 00000000000..29760f79c3c
--- /dev/null
+++ b/spec/javascripts/related_merge_requests/components/related_merge_requests_spec.js
@@ -0,0 +1,89 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue';
+import createStore from '~/related_merge_requests/store/index';
+
+const FIXTURE_PATH = 'issues/related_merge_requests.json';
+const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
+const localVue = createLocalVue();
+
+describe('RelatedMergeRequests', () => {
+ let wrapper;
+ let mock;
+ let mockData;
+
+ beforeEach(done => {
+ loadFixtures(FIXTURE_PATH);
+ mockData = getJSONFixture(FIXTURE_PATH);
+ mock = new MockAdapter(axios);
+ mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
+
+ wrapper = mount(RelatedMergeRequests, {
+ localVue,
+ sync: false,
+ store: createStore(),
+ propsData: {
+ endpoint: API_ENDPOINT,
+ projectNamespace: 'gitlab-org',
+ projectPath: 'gitlab-ce',
+ },
+ });
+
+ setTimeout(done);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ mock.restore();
+ });
+
+ describe('methods', () => {
+ describe('getAssignees', () => {
+ const assignees = [{ name: 'foo' }, { name: 'bar' }];
+
+ describe('when there is assignees array', () => {
+ it('should return assignees array', () => {
+ const mr = { assignees };
+
+ expect(wrapper.vm.getAssignees(mr)).toEqual(assignees);
+ });
+ });
+
+ it('should return an array with single assingee', () => {
+ const mr = { assignee: assignees[0] };
+
+ expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]);
+ });
+
+ it('should return empty array when assignee is not set', () => {
+ expect(wrapper.vm.getAssignees({})).toEqual([]);
+ expect(wrapper.vm.getAssignees({ assignee: null })).toEqual([]);
+ });
+ });
+ });
+
+ 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);
+
+ const props = wrapper
+ .findAll(RelatedIssuableItem)
+ .at(1)
+ .props();
+ const data = mockData[1];
+
+ expect(props.idKey).toEqual(data.id);
+ expect(props.pathIdSeparator).toEqual('!');
+ expect(props.pipelineStatus).toBe(data.head_pipeline.detailed_status);
+ expect(props.assignees).toEqual([data.assignee]);
+ expect(props.isMergeRequest).toBe(true);
+ expect(props.confidential).toEqual(false);
+ expect(props.title).toEqual(data.title);
+ expect(props.state).toEqual(data.state);
+ expect(props.createdAt).toEqual(data.created_at);
+ });
+ });
+});
diff --git a/spec/javascripts/related_merge_requests/store/actions_spec.js b/spec/javascripts/related_merge_requests/store/actions_spec.js
new file mode 100644
index 00000000000..65e436fbb17
--- /dev/null
+++ b/spec/javascripts/related_merge_requests/store/actions_spec.js
@@ -0,0 +1,110 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import * as types from '~/related_merge_requests/store/mutation_types';
+import actionsModule, * as actions from '~/related_merge_requests/store/actions';
+import testAction from 'spec/helpers/vuex_action_helper';
+
+describe('RelatedMergeRequest store actions', () => {
+ let state;
+ let flashSpy;
+ let mock;
+
+ beforeEach(() => {
+ state = {
+ apiEndpoint: '/api/related_merge_requests',
+ };
+ flashSpy = spyOnDependency(actionsModule, 'createFlash');
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setInitialState', () => {
+ it('commits types.SET_INITIAL_STATE with given props', done => {
+ const props = { a: 1, b: 2 };
+
+ testAction(
+ actions.setInitialState,
+ props,
+ {},
+ [{ type: types.SET_INITIAL_STATE, payload: props }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestData', () => {
+ it('commits types.REQUEST_DATA', done => {
+ testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done);
+ });
+ });
+
+ describe('receiveDataSuccess', () => {
+ it('commits types.RECEIVE_DATA_SUCCESS with data', done => {
+ const data = { a: 1, b: 2 };
+
+ testAction(
+ actions.receiveDataSuccess,
+ data,
+ {},
+ [{ type: types.RECEIVE_DATA_SUCCESS, payload: data }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('receiveDataError', () => {
+ it('commits types.RECEIVE_DATA_ERROR', done => {
+ testAction(
+ actions.receiveDataError,
+ null,
+ {},
+ [{ type: types.RECEIVE_DATA_ERROR }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('fetchMergeRequests', () => {
+ describe('for a successful request', () => {
+ it('should dispatch success action', done => {
+ const data = { a: 1 };
+ mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
+
+ testAction(
+ actions.fetchMergeRequests,
+ null,
+ state,
+ [],
+ [{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }],
+ done,
+ );
+ });
+ });
+
+ describe('for a failing request', () => {
+ it('should dispatch error action', done => {
+ mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
+
+ testAction(
+ actions.fetchMergeRequests,
+ null,
+ state,
+ [],
+ [{ type: 'requestData' }, { type: 'receiveDataError' }],
+ () => {
+ expect(flashSpy).toHaveBeenCalledTimes(1);
+ expect(flashSpy).toHaveBeenCalledWith(jasmine.stringMatching('Something went wrong'));
+
+ done();
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/related_merge_requests/store/mutations_spec.js b/spec/javascripts/related_merge_requests/store/mutations_spec.js
new file mode 100644
index 00000000000..21b6e26376b
--- /dev/null
+++ b/spec/javascripts/related_merge_requests/store/mutations_spec.js
@@ -0,0 +1,49 @@
+import mutations from '~/related_merge_requests/store/mutations';
+import * as types from '~/related_merge_requests/store/mutation_types';
+
+describe('RelatedMergeRequests Store Mutations', () => {
+ describe('SET_INITIAL_STATE', () => {
+ it('should set initial state according to given data', () => {
+ const apiEndpoint = '/api';
+ const state = {};
+
+ mutations[types.SET_INITIAL_STATE](state, { apiEndpoint });
+
+ expect(state.apiEndpoint).toEqual(apiEndpoint);
+ });
+ });
+
+ describe('REQUEST_DATA', () => {
+ it('should set loading flag', () => {
+ const state = {};
+
+ mutations[types.REQUEST_DATA](state);
+
+ expect(state.isFetchingMergeRequests).toEqual(true);
+ });
+ });
+
+ describe('RECEIVE_DATA_SUCCESS', () => {
+ it('should set loading flag and data', () => {
+ const state = {};
+ const mrs = [1, 2, 3];
+
+ mutations[types.RECEIVE_DATA_SUCCESS](state, { data: mrs, total: mrs.length });
+
+ expect(state.isFetchingMergeRequests).toEqual(false);
+ expect(state.mergeRequests).toEqual(mrs);
+ expect(state.totalCount).toEqual(mrs.length);
+ });
+ });
+
+ describe('RECEIVE_DATA_ERROR', () => {
+ it('should set loading and error flags', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_DATA_ERROR](state);
+
+ expect(state.isFetchingMergeRequests).toEqual(false);
+ expect(state.hasErrorFetchingMergeRequests).toEqual(true);
+ });
+ });
+});
diff --git a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
index 69767d9cf1c..a17494966a3 100644
--- a/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
+++ b/spec/javascripts/reports/components/grouped_test_reports_app_spec.js
@@ -61,7 +61,7 @@ describe('Grouped Test Reports App', () => {
it('renders success summary text', done => {
setTimeout(() => {
- expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull();
+ expect(vm.$el.querySelector('.spinner')).not.toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary results are being parsed',
);
@@ -81,7 +81,7 @@ describe('Grouped Test Reports App', () => {
it('renders failed summary text + new badge', done => {
setTimeout(() => {
- expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
+ expect(vm.$el.querySelector('.spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary contained 2 failed test results out of 11 total tests',
);
@@ -109,7 +109,7 @@ describe('Grouped Test Reports App', () => {
it('renders summary text', done => {
setTimeout(() => {
- expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
+ expect(vm.$el.querySelector('.spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary contained 2 failed test results and 2 fixed test results out of 11 total tests',
);
@@ -137,7 +137,7 @@ describe('Grouped Test Reports App', () => {
it('renders summary text', done => {
setTimeout(() => {
- expect(vm.$el.querySelector('.fa-spinner')).toBeNull();
+ expect(vm.$el.querySelector('.spinner')).toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary contained 2 fixed test results out of 11 total tests',
);
@@ -190,7 +190,7 @@ describe('Grouped Test Reports App', () => {
});
it('renders loading summary text with loading icon', done => {
- expect(vm.$el.querySelector('.fa-spinner')).not.toBeNull();
+ expect(vm.$el.querySelector('.spinner')).not.toBeNull();
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
'Test summary results are being parsed',
);
diff --git a/spec/javascripts/reports/components/modal_spec.js b/spec/javascripts/reports/components/modal_spec.js
index 6b8471381de..d42c509e5b5 100644
--- a/spec/javascripts/reports/components/modal_spec.js
+++ b/spec/javascripts/reports/components/modal_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import component from '~/reports/components/modal.vue';
import state from '~/reports/store/state';
import mountComponent from '../../helpers/vue_mount_component_helper';
-import { trimText } from '../../helpers/vue_component_helper';
+import { trimText } from '../../helpers/text_helper';
describe('Grouped Test Reports Modal', () => {
const Component = Vue.extend(component);
diff --git a/spec/javascripts/reports/components/test_issue_body_spec.js b/spec/javascripts/reports/components/test_issue_body_spec.js
index 32baf904ad7..9c1cec4c9bc 100644
--- a/spec/javascripts/reports/components/test_issue_body_spec.js
+++ b/spec/javascripts/reports/components/test_issue_body_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import component from '~/reports/components/test_issue_body.vue';
import createStore from '~/reports/store';
import { mountComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { trimText } from '../../helpers/vue_component_helper';
+import { trimText } from '../../helpers/text_helper';
import { issue } from '../mock_data/mock_data';
describe('Test Issue body', () => {
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 992e17978c1..9565e3ce546 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -23,7 +23,7 @@ const assertSidebarState = function(state) {
describe('RightSidebar', function() {
describe('fixture tests', () => {
- const fixtureName = 'issues/open-issue.html.raw';
+ const fixtureName = 'issues/open-issue.html';
preloadFixtures(fixtureName);
loadJSONFixtures('todos/todos.json');
let mock;
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 7a4ca587313..ce7fa7a52ae 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -126,9 +126,9 @@ describe('Search autocomplete dropdown', () => {
expect(list.find(mrsIHaveCreatedLink).text()).toBe("Merge requests I've created");
};
- preloadFixtures('static/search_autocomplete.html.raw');
+ preloadFixtures('static/search_autocomplete.html');
beforeEach(function() {
- loadFixtures('static/search_autocomplete.html.raw');
+ loadFixtures('static/search_autocomplete.html');
window.gon = {};
window.gon.current_user_id = userId;
diff --git a/spec/javascripts/search_spec.js b/spec/javascripts/search_spec.js
index 40bdbac7451..32f60508fa3 100644
--- a/spec/javascripts/search_spec.js
+++ b/spec/javascripts/search_spec.js
@@ -3,7 +3,7 @@ import Api from '~/api';
import Search from '~/pages/search/show/search';
describe('Search', () => {
- const fixturePath = 'search/show.html.raw';
+ const fixturePath = 'search/show.html';
const searchTerm = 'some search';
const fillDropdownInput = dropdownSelector => {
const dropdownElement = document.querySelector(dropdownSelector).parentNode;
diff --git a/spec/javascripts/serverless/components/function_row_spec.js b/spec/javascripts/serverless/components/function_row_spec.js
deleted file mode 100644
index 6933a8f6c87..00000000000
--- a/spec/javascripts/serverless/components/function_row_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Vue from 'vue';
-
-import functionRowComponent from '~/serverless/components/function_row.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-import { mockServerlessFunction } from '../mock_data';
-
-const createComponent = func => mountComponent(Vue.extend(functionRowComponent), { func });
-
-describe('functionRowComponent', () => {
- it('Parses the function details correctly', () => {
- const vm = createComponent(mockServerlessFunction);
-
- expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name);
- expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image);
- expect(vm.$el.querySelector('time').getAttribute('data-original-title')).not.toBe(null);
- expect(vm.$el.querySelector('div.url-text-field').innerHTML).toEqual(
- mockServerlessFunction.url,
- );
-
- vm.$destroy();
- });
-
- it('handles clicks correctly', () => {
- const vm = createComponent(mockServerlessFunction);
-
- expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row
- expect(vm.checkClass(vm.$el.querySelector('svg'))).toBe(false); // check a button image
- expect(vm.checkClass(vm.$el.querySelector('div.url-text-field'))).toBe(false); // check the url bar
-
- vm.$destroy();
- });
-});
diff --git a/spec/javascripts/serverless/components/functions_spec.js b/spec/javascripts/serverless/components/functions_spec.js
deleted file mode 100644
index 85cfe71281f..00000000000
--- a/spec/javascripts/serverless/components/functions_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import Vue from 'vue';
-
-import functionsComponent from '~/serverless/components/functions.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ServerlessStore from '~/serverless/stores/serverless_store';
-
-import { mockServerlessFunctions } from '../mock_data';
-
-const createComponent = (
- functions,
- installed = true,
- loadingData = true,
- hasFunctionData = true,
-) => {
- const component = Vue.extend(functionsComponent);
-
- return mountComponent(component, {
- functions,
- installed,
- clustersPath: '/testClusterPath',
- helpPath: '/helpPath',
- loadingData,
- hasFunctionData,
- });
-};
-
-describe('functionsComponent', () => {
- it('should render empty state when Knative is not installed', () => {
- const vm = createComponent({}, false);
-
- expect(vm.$el.querySelector('div.row').classList.contains('js-empty-state')).toBe(true);
- expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual(
- 'Getting started with serverless',
- );
-
- vm.$destroy();
- });
-
- it('should render a loading component', () => {
- const vm = createComponent({});
-
- expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBe(null);
- expect(vm.$el.querySelector('div.animation-container')).not.toBe(null);
- });
-
- it('should render empty state when there is no function data', () => {
- const vm = createComponent({}, true, false, false);
-
- expect(
- vm.$el.querySelector('.empty-state, .js-empty-state').classList.contains('js-empty-state'),
- ).toBe(true);
-
- expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual(
- 'No functions available',
- );
-
- vm.$destroy();
- });
-
- it('should render the functions list', () => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctions);
- const vm = createComponent(store.state.functions, true, false);
-
- expect(vm.$el.querySelector('div.groups-list-tree-container')).not.toBe(null);
- expect(vm.$el.querySelector('#env-global').classList.contains('has-children')).toBe(true);
- });
-});
diff --git a/spec/javascripts/serverless/mock_data.js b/spec/javascripts/serverless/mock_data.js
deleted file mode 100644
index ecd393b174c..00000000000
--- a/spec/javascripts/serverless/mock_data.js
+++ /dev/null
@@ -1,79 +0,0 @@
-export const mockServerlessFunctions = [
- {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
- },
- {
- name: 'testfunc2',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc2.tm-example.apps.example.com',
- description: 'A second test service\nThis one with additional descriptions',
- image: 'knative-test-echo-buildtemplate',
- },
-];
-
-export const mockServerlessFunctionsDiffEnv = [
- {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
- },
- {
- name: 'testfunc2',
- namespace: 'tm-example',
- environment_scope: 'test',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc2',
- podcount: null,
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc2.tm-example.apps.example.com',
- description: 'A second test service\nThis one with additional descriptions',
- image: 'knative-test-echo-buildtemplate',
- },
-];
-
-export const mockServerlessFunction = {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: '3',
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'A test service',
- image: 'knative-test-container-buildtemplate',
-};
-
-export const mockMultilineServerlessFunction = {
- name: 'testfunc1',
- namespace: 'tm-example',
- environment_scope: '*',
- cluster_id: 46,
- detail_url: '/testuser/testproj/serverless/functions/*/testfunc1',
- podcount: '3',
- created_at: '2019-02-05T01:01:23Z',
- url: 'http://testfunc1.tm-example.apps.example.com',
- description: 'testfunc1\nA test service line\\nWith additional services',
- image: 'knative-test-container-buildtemplate',
-};
diff --git a/spec/javascripts/serverless/stores/serverless_store_spec.js b/spec/javascripts/serverless/stores/serverless_store_spec.js
deleted file mode 100644
index 72fd903d7d1..00000000000
--- a/spec/javascripts/serverless/stores/serverless_store_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import ServerlessStore from '~/serverless/stores/serverless_store';
-import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
-
-describe('Serverless Functions Store', () => {
- let store;
-
- beforeEach(() => {
- store = new ServerlessStore(false, '/cluster_path', 'help_path');
- });
-
- describe('#updateFunctionsFromServer', () => {
- it('should pass an empty hash object', () => {
- store.updateFunctionsFromServer();
-
- expect(store.state.functions).toEqual({});
- });
-
- it('should group functions to one global environment', () => {
- const mockServerlessData = mockServerlessFunctions;
- store.updateFunctionsFromServer(mockServerlessData);
-
- expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*']));
- expect(store.state.functions['*'].length).toEqual(2);
- });
-
- it('should group functions to multiple environments', () => {
- const mockServerlessData = mockServerlessFunctionsDiffEnv;
- store.updateFunctionsFromServer(mockServerlessData);
-
- expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*']));
- expect(store.state.functions['*'].length).toEqual(1);
- expect(store.state.functions.test.length).toEqual(1);
- expect(store.state.functions.test[0].name).toEqual('testfunc2');
- });
- });
-});
diff --git a/spec/javascripts/settings_panels_spec.js b/spec/javascripts/settings_panels_spec.js
index 3b681a9ff28..2c5d91a45bc 100644
--- a/spec/javascripts/settings_panels_spec.js
+++ b/spec/javascripts/settings_panels_spec.js
@@ -2,10 +2,10 @@ import $ from 'jquery';
import initSettingsPanels from '~/settings_panels';
describe('Settings Panels', () => {
- preloadFixtures('groups/edit.html.raw');
+ preloadFixtures('groups/edit.html');
beforeEach(() => {
- loadFixtures('groups/edit.html.raw');
+ loadFixtures('groups/edit.html');
});
describe('initSettingsPane', () => {
diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js
index 3ca6ecaa938..df7012bb659 100644
--- a/spec/javascripts/shortcuts_spec.js
+++ b/spec/javascripts/shortcuts_spec.js
@@ -2,7 +2,7 @@ import $ from 'jquery';
import Shortcuts from '~/behaviors/shortcuts/shortcuts';
describe('Shortcuts', () => {
- const fixtureName = 'snippets/show.html.raw';
+ const fixtureName = 'snippets/show.html';
const createEvent = (type, target) =>
$.Event(type, {
target,
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index eced4925489..4ae2141d5f0 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -24,12 +24,12 @@ describe('Assignee component', () => {
const collapsed = component.$el.querySelector('.sidebar-collapsed-icon');
expect(collapsed.childElementCount).toEqual(1);
- expect(collapsed.children[0].getAttribute('aria-label')).toEqual('No Assignee');
+ expect(collapsed.children[0].getAttribute('aria-label')).toEqual('None');
expect(collapsed.children[0].classList.contains('fa')).toEqual(true);
expect(collapsed.children[0].classList.contains('fa-user')).toEqual(true);
});
- it('displays only "No assignee" when no users are assigned and the issue is read-only', () => {
+ it('displays only "None" when no users are assigned and the issue is read-only', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
@@ -39,11 +39,11 @@ describe('Assignee component', () => {
}).$mount();
const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
- expect(componentTextNoUsers).toBe('No assignee');
+ expect(componentTextNoUsers).toBe('None');
expect(componentTextNoUsers.indexOf('assign yourself')).toEqual(-1);
});
- it('displays only "No assignee" when no users are assigned and the issue can be edited', () => {
+ it('displays only "None" when no users are assigned and the issue can be edited', () => {
component = new AssigneeComponent({
propsData: {
rootPath: 'http://localhost:3000',
@@ -53,7 +53,7 @@ describe('Assignee component', () => {
}).$mount();
const componentTextNoUsers = component.$el.querySelector('.assign-yourself').innerText.trim();
- expect(componentTextNoUsers.indexOf('No assignee')).toEqual(0);
+ expect(componentTextNoUsers.indexOf('None')).toEqual(0);
expect(componentTextNoUsers.indexOf('assign yourself')).toBeGreaterThan(0);
});
@@ -132,9 +132,94 @@ describe('Assignee component', () => {
-1,
);
});
+
+ it('has correct "cannot merge" tooltip when user cannot merge', () => {
+ const user = Object.assign({}, UsersMock.user, { can_merge: false });
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users: [user],
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('Cannot merge');
+ });
});
describe('Two or more assignees/users', () => {
+ it('has correct "cannot merge" tooltip when one user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = true;
+ users[1].can_merge = false;
+ users[2].can_merge = false;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('1/3 can merge');
+ });
+
+ it('has correct "cannot merge" tooltip when no user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ users[0].can_merge = false;
+ users[1].can_merge = false;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('No one can merge');
+ });
+
+ it('has correct "cannot merge" tooltip when more than one user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(3);
+ users[0].can_merge = false;
+ users[1].can_merge = true;
+ users[2].can_merge = true;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual('2/3 can merge');
+ });
+
+ it('has no "cannot merge" tooltip when every user can merge', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ users[0].can_merge = true;
+ users[1].can_merge = true;
+
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000/',
+ users,
+ editable: true,
+ issuableType: 'merge_request',
+ },
+ }).$mount();
+
+ expect(component.mergeNotAllowedTooltipMessage).toEqual(null);
+ });
+
it('displays two assignee icons when collapsed', () => {
const users = UsersMockHelper.createNumberRandomUsers(2);
component = new AssigneeComponent({
@@ -210,6 +295,19 @@ describe('Assignee component', () => {
expect(component.$el.querySelector('.user-list-more')).toBe(null);
});
+ it('sets tooltip container to body', () => {
+ const users = UsersMockHelper.createNumberRandomUsers(2);
+ component = new AssigneeComponent({
+ propsData: {
+ rootPath: 'http://localhost:3000',
+ users,
+ editable: true,
+ },
+ }).$mount();
+
+ expect(component.$el.querySelector('.user-link').getAttribute('data-container')).toBe('body');
+ });
+
it('Shows the "show-less" assignees label', done => {
const users = UsersMockHelper.createNumberRandomUsers(6);
component = new AssigneeComponent({
diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js
index 3f0f67d71ca..016f5e033a5 100644
--- a/spec/javascripts/sidebar/sidebar_assignees_spec.js
+++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js
@@ -11,12 +11,12 @@ describe('sidebar assignees', () => {
let vm;
let mediator;
let sidebarAssigneesEl;
- preloadFixtures('issues/open-issue.html.raw');
+ preloadFixtures('issues/open-issue.html');
beforeEach(() => {
Vue.http.interceptors.push(Mock.sidebarMockInterceptor);
- loadFixtures('issues/open-issue.html.raw');
+ loadFixtures('issues/open-issue.html');
mediator = new SidebarMediator(Mock.mediator);
spyOn(mediator, 'saveAssignees').and.callThrough();
diff --git a/spec/javascripts/sidebar/todo_spec.js b/spec/javascripts/sidebar/todo_spec.js
index 657e88ecb96..f46ea5a0499 100644
--- a/spec/javascripts/sidebar/todo_spec.js
+++ b/spec/javascripts/sidebar/todo_spec.js
@@ -116,7 +116,7 @@ describe('SidebarTodo', () => {
const dataAttributes = {
issuableId: '1',
issuableType: 'epic',
- originalTitle: 'Mark todo as done',
+ originalTitle: '',
placement: 'left',
container: 'body',
boundary: 'viewport',
@@ -130,6 +130,10 @@ describe('SidebarTodo', () => {
});
});
+ it('check button label computed property', () => {
+ expect(vm.buttonLabel).toEqual('Mark todo as done');
+ });
+
it('renders button label element when `collapsed` prop is `false`', () => {
const buttonLabelEl = vm.$el.querySelector('span.issuable-todo-inner');
diff --git a/spec/javascripts/signin_tabs_memoizer_spec.js b/spec/javascripts/signin_tabs_memoizer_spec.js
index 52da6a79939..ef5c774736b 100644
--- a/spec/javascripts/signin_tabs_memoizer_spec.js
+++ b/spec/javascripts/signin_tabs_memoizer_spec.js
@@ -2,7 +2,7 @@ import AccessorUtilities from '~/lib/utils/accessor';
import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
describe('SigninTabsMemoizer', () => {
- const fixtureTemplate = 'static/signin_tabs.html.raw';
+ const fixtureTemplate = 'static/signin_tabs.html';
const tabSelector = 'ul.new-session-tabs';
const currentTabKey = 'current_signin_tab';
let memo;
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index b2b0a50911d..8c80a425581 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -8,6 +8,7 @@ import '~/commons';
import Vue from 'vue';
import VueResource from 'vue-resource';
import Translate from '~/vue_shared/translate';
+import CheckEE from '~/vue_shared/mixins/is_ee';
import jasmineDiff from 'jasmine-diff';
import { getDefaultAdapter } from '~/lib/utils/axios_utils';
@@ -43,6 +44,7 @@ Vue.config.errorHandler = function(err) {
Vue.use(VueResource);
Vue.use(Translate);
+Vue.use(CheckEE);
// enable test fixtures
jasmine.getFixtures().fixturesPath = FIXTURES_PATH;
@@ -67,6 +69,7 @@ window.gl = window.gl || {};
window.gl.TEST_HOST = TEST_HOST;
window.gon = window.gon || {};
window.gon.test_env = true;
+window.gon.ee = process.env.IS_GITLAB_EE;
gon.relative_url_root = '';
let hasUnhandledPromiseRejections = false;
@@ -108,7 +111,7 @@ let longRunningTestTimeoutHandle;
beforeEach(done => {
longRunningTestTimeoutHandle = setTimeout(() => {
done.fail('Test is running too long!');
- }, 2000);
+ }, 4000);
done();
});
@@ -119,19 +122,26 @@ afterEach(() => {
const axiosDefaultAdapter = getDefaultAdapter();
// render all of our tests
-const testsContext = require.context('.', true, /_spec$/);
-testsContext.keys().forEach(function(path) {
- try {
- testsContext(path);
- } catch (err) {
- console.log(err);
- console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
- describe('Test bundle', function() {
- it(`includes '${path}'`, function() {
- expect(err).toBeNull();
+const testContexts = [require.context('spec', true, /_spec$/)];
+
+if (process.env.IS_GITLAB_EE) {
+ testContexts.push(require.context('ee_spec', true, /_spec$/));
+}
+
+testContexts.forEach(context => {
+ context.keys().forEach(path => {
+ try {
+ context(path);
+ } catch (err) {
+ console.log(err);
+ console.error('[GL SPEC RUNNER ERROR] Unable to load spec: ', path);
+ describe('Test bundle', function() {
+ it(`includes '${path}'`, function() {
+ expect(err).toBeNull();
+ });
});
- });
- }
+ }
+ });
});
describe('test errors', () => {
@@ -201,24 +211,35 @@ if (process.env.BABEL_ENV === 'coverage') {
];
describe('Uncovered files', function() {
- const sourceFiles = require.context('~', true, /\.(js|vue)$/);
+ const sourceFilesContexts = [require.context('~', true, /\.(js|vue)$/)];
+
+ if (process.env.IS_GITLAB_EE) {
+ sourceFilesContexts.push(require.context('ee', true, /\.(js|vue)$/));
+ }
+
+ const allTestFiles = testContexts.reduce(
+ (accumulator, context) => accumulator.concat(context.keys()),
+ [],
+ );
$.holdReady(true);
- sourceFiles.keys().forEach(function(path) {
- // ignore if there is a matching spec file
- if (testsContext.keys().indexOf(`${path.replace(/\.(js|vue)$/, '')}_spec`) > -1) {
- return;
- }
-
- it(`includes '${path}'`, function() {
- try {
- sourceFiles(path);
- } catch (err) {
- if (troubleMakers.indexOf(path) === -1) {
- expect(err).toBeNull();
- }
+ sourceFilesContexts.forEach(context => {
+ context.keys().forEach(path => {
+ // ignore if there is a matching spec file
+ if (allTestFiles.indexOf(`${path.replace(/\.(js|vue)$/, '')}_spec`) > -1) {
+ return;
}
+
+ it(`includes '${path}'`, function() {
+ try {
+ context(path);
+ } catch (err) {
+ if (troubleMakers.indexOf(path) === -1) {
+ expect(err).toBeNull();
+ }
+ }
+ });
});
});
});
diff --git a/spec/javascripts/test_constants.js b/spec/javascripts/test_constants.js
index a820dd2d09c..77c206585fe 100644
--- a/spec/javascripts/test_constants.js
+++ b/spec/javascripts/test_constants.js
@@ -1,7 +1,9 @@
-export const FIXTURES_PATH = '/base/spec/javascripts/fixtures';
+export const FIXTURES_PATH = `/base/${
+ process.env.IS_GITLAB_EE ? 'ee/' : ''
+}spec/javascripts/fixtures`;
export const TEST_HOST = 'http://test.host';
-export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/one_white_pixel.png`;
+export const DUMMY_IMAGE_URL = `${FIXTURES_PATH}/static/images/one_white_pixel.png`;
-export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/green_box.png`;
-export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/images/red_box.png`;
+export const GREEN_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/green_box.png`;
+export const RED_BOX_IMAGE_URL = `${FIXTURES_PATH}/static/images/red_box.png`;
diff --git a/spec/javascripts/todos_spec.js b/spec/javascripts/todos_spec.js
index 69e43274250..802f54f6a7e 100644
--- a/spec/javascripts/todos_spec.js
+++ b/spec/javascripts/todos_spec.js
@@ -3,11 +3,11 @@ import Todos from '~/pages/dashboard/todos/index/todos';
import '~/lib/utils/common_utils';
describe('Todos', () => {
- preloadFixtures('todos/todos.html.raw');
+ preloadFixtures('todos/todos.html');
let todoItem;
beforeEach(() => {
- loadFixtures('todos/todos.html.raw');
+ loadFixtures('todos/todos.html');
todoItem = document.querySelector('.todos-list .todo');
return new Todos();
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index ddb09811dda..8f9cb270729 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -4,10 +4,10 @@ import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
describe('U2FAuthenticate', function() {
- preloadFixtures('u2f/authenticate.html.raw');
+ preloadFixtures('u2f/authenticate.html');
beforeEach(() => {
- loadFixtures('u2f/authenticate.html.raw');
+ loadFixtures('u2f/authenticate.html');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-authenticate-u2f');
this.component = new U2FAuthenticate(
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 261db3d66d7..a75ceca9f4c 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -4,10 +4,10 @@ import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
describe('U2FRegister', function() {
- preloadFixtures('u2f/register.html.raw');
+ preloadFixtures('u2f/register.html');
beforeEach(done => {
- loadFixtures('u2f/register.html.raw');
+ loadFixtures('u2f/register.html');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-register-u2f');
this.component = new U2FRegister(this.container, $('#js-register-u2f-templates'), {}, 'token');
diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js
index b174a51c1a0..c0d5ee9c446 100644
--- a/spec/javascripts/user_popovers_spec.js
+++ b/spec/javascripts/user_popovers_spec.js
@@ -2,7 +2,7 @@ import initUserPopovers from '~/user_popovers';
import UsersCache from '~/lib/utils/users_cache';
describe('User Popovers', () => {
- const fixtureTemplate = 'merge_requests/diff_comment.html.raw';
+ const fixtureTemplate = 'merge_requests/diff_comment.html';
preloadFixtures(fixtureTemplate);
const selector = '.js-user-link';
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js
new file mode 100644
index 00000000000..8ec17efffb9
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_alert_message_spec.js
@@ -0,0 +1,77 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
+import { GlLink } from '@gitlab/ui';
+
+describe('MrWidgetAlertMessage', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const localVue = createLocalVue();
+
+ wrapper = shallowMount(localVue.extend(MrWidgetAlertMessage), {
+ propsData: {},
+ localVue,
+ sync: false,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when type is not provided', () => {
+ it('should render a red message', done => {
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('danger_message');
+ expect(wrapper.classes()).not.toContain('warning_message');
+ done();
+ });
+ });
+ });
+
+ describe('when type === "danger"', () => {
+ it('should render a red message', done => {
+ wrapper.setProps({ type: 'danger' });
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('danger_message');
+ expect(wrapper.classes()).not.toContain('warning_message');
+ done();
+ });
+ });
+ });
+
+ describe('when type === "warning"', () => {
+ it('should render a red message', done => {
+ wrapper.setProps({ type: 'warning' });
+ wrapper.vm.$nextTick(() => {
+ expect(wrapper.classes()).toContain('warning_message');
+ expect(wrapper.classes()).not.toContain('danger_message');
+ done();
+ });
+ });
+ });
+
+ describe('when helpPath is not provided', () => {
+ it('should not render a help icon/link', done => {
+ wrapper.vm.$nextTick(() => {
+ const link = wrapper.find(GlLink);
+
+ expect(link.exists()).toBe(false);
+ done();
+ });
+ });
+ });
+
+ describe('when helpPath is provided', () => {
+ it('should render a help icon/link', done => {
+ wrapper.setProps({ helpPath: '/path/to/help/docs' });
+ wrapper.vm.$nextTick(() => {
+ const link = wrapper.find(GlLink);
+
+ expect(link.exists()).toBe(true);
+ expect(link.attributes().href).toBe('/path/to/help/docs');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 02c476f2871..cd77b0ab815 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -15,6 +15,16 @@ describe('MRWidgetHeader', () => {
gon.relative_url_root = '';
});
+ const expectDownloadDropdownItems = () => {
+ const downloadEmailPatchesEl = vm.$el.querySelector('.js-download-email-patches');
+ const downloadPlainDiffEl = vm.$el.querySelector('.js-download-plain-diff');
+
+ expect(downloadEmailPatchesEl.textContent.trim()).toEqual('Email patches');
+ expect(downloadEmailPatchesEl.getAttribute('href')).toEqual('/mr/email-patches');
+ expect(downloadPlainDiffEl.textContent.trim()).toEqual('Plain diff');
+ expect(downloadPlainDiffEl.getAttribute('href')).toEqual('/mr/plainDiffPath');
+ };
+
describe('computed', () => {
describe('shouldShowCommitsBehindText', () => {
it('return true when there are divergedCommitsCount', () => {
@@ -207,21 +217,7 @@ describe('MRWidgetHeader', () => {
});
it('renders download dropdown with links', () => {
- expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual(
- 'Email patches',
- );
-
- expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual(
- '/mr/email-patches',
- );
-
- expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual(
- 'Plain diff',
- );
-
- expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual(
- '/mr/plainDiffPath',
- );
+ expectDownloadDropdownItems();
});
});
@@ -250,10 +246,8 @@ describe('MRWidgetHeader', () => {
expect(button).toEqual(null);
});
- it('does not render download dropdown with links', () => {
- expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null);
-
- expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null);
+ it('renders download dropdown with links', () => {
+ expectDownloadDropdownItems();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
index e5155573f6f..dfbc68c48b9 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { mount, createLocalVue } from '@vue/test-utils';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import { mockStore } from '../mock_data';
@@ -9,7 +9,7 @@ describe('MrWidgetPipelineContainer', () => {
const factory = (props = {}) => {
const localVue = createLocalVue();
- wrapper = shallowMount(localVue.extend(MrWidgetPipelineContainer), {
+ wrapper = mount(localVue.extend(MrWidgetPipelineContainer), {
propsData: {
mr: Object.assign({}, mockStore),
...props,
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index d905bbe4040..a2308b0dfdb 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { trimText } from 'spec/helpers/text_helper';
import mockData from '../mock_data';
describe('MRWidgetPipeline', () => {
@@ -77,6 +78,19 @@ describe('MRWidgetPipeline', () => {
);
});
+ it('should render CI error when no pipeline is provided', () => {
+ vm = mountComponent(Component, {
+ pipeline: {},
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.',
+ );
+ });
+
describe('with a pipeline', () => {
beforeEach(() => {
vm = mountComponent(Component, {
@@ -89,7 +103,7 @@ describe('MRWidgetPipeline', () => {
it('should render pipeline ID', () => {
expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
- `#${mockData.pipeline.id}`,
+ `#${mockData.pipeline.id} (#${mockData.pipeline.iid})`,
);
});
@@ -123,7 +137,7 @@ describe('MRWidgetPipeline', () => {
describe('without commit path', () => {
beforeEach(() => {
- const mockCopy = Object.assign({}, mockData);
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.commit;
vm = mountComponent(Component, {
@@ -136,7 +150,7 @@ describe('MRWidgetPipeline', () => {
it('should render pipeline ID', () => {
expect(vm.$el.querySelector('.pipeline-id').textContent.trim()).toEqual(
- `#${mockData.pipeline.id}`,
+ `#${mockData.pipeline.id} (#${mockData.pipeline.iid})`,
);
});
@@ -164,7 +178,7 @@ describe('MRWidgetPipeline', () => {
describe('without coverage', () => {
it('should not render a coverage', () => {
- const mockCopy = Object.assign({}, mockData);
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.coverage;
vm = mountComponent(Component, {
@@ -180,7 +194,7 @@ describe('MRWidgetPipeline', () => {
describe('without a pipeline graph', () => {
it('should not render a pipeline graph', () => {
- const mockCopy = Object.assign({}, mockData);
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
delete mockCopy.pipeline.details.stages;
vm = mountComponent(Component, {
@@ -193,5 +207,83 @@ describe('MRWidgetPipeline', () => {
expect(vm.$el.querySelector('.js-mini-pipeline-graph')).toEqual(null);
});
});
+
+ describe('without pipeline.merge_request', () => {
+ it('should render info that includes the commit and branch details', () => {
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ delete mockCopy.pipeline.merge_request;
+ const { pipeline } = mockCopy;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ sourceBranchLink: mockCopy.source_branch_link,
+ });
+
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on ${mockCopy.source_branch_link}`;
+
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+
+ expect(actual).toBe(expected);
+ });
+ });
+
+ describe('with pipeline.merge_request and flags.merge_request_pipeline', () => {
+ it('should render info that includes the commit, MR, source branch, and target branch details', () => {
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ const { pipeline } = mockCopy;
+ pipeline.flags.merge_request_pipeline = true;
+ pipeline.flags.detached_merge_request_pipeline = false;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ sourceBranchLink: mockCopy.source_branch_link,
+ });
+
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${
+ pipeline.merge_request.source_branch
+ } into ${pipeline.merge_request.target_branch}`;
+
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+
+ expect(actual).toBe(expected);
+ });
+ });
+
+ describe('with pipeline.merge_request and flags.detached_merge_request_pipeline', () => {
+ it('should render info that includes the commit, MR, and source branch details', () => {
+ const mockCopy = JSON.parse(JSON.stringify(mockData));
+ const { pipeline } = mockCopy;
+ pipeline.flags.merge_request_pipeline = false;
+ pipeline.flags.detached_merge_request_pipeline = true;
+
+ vm = mountComponent(Component, {
+ pipeline,
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ sourceBranchLink: mockCopy.source_branch_link,
+ });
+
+ const expected = `Pipeline #${pipeline.id} (#${pipeline.iid}) ${
+ pipeline.details.status.label
+ } for ${pipeline.commit.short_id} on !${pipeline.merge_request.iid} with ${
+ pipeline.merge_request.source_branch
+ }`;
+
+ const actual = trimText(vm.$el.querySelector('.js-pipeline-info-container').innerText);
+
+ expect(actual).toBe(expected);
+ });
+ });
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
index a0a336ae604..f622f52a7b9 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
@@ -18,7 +18,7 @@ describe('MR widget status icon component', () => {
it('renders loading icon', () => {
vm = mountComponent(Component, { status: 'loading' });
- expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner');
+ expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('spinner');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
index eb4fa0df727..d93badf8cd3 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -38,7 +38,7 @@ describe('MRWidgetAutoMergeFailed', () => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled');
- expect(vm.$el.querySelector('button i').classList).toContain('fa-spinner');
+ expect(vm.$el.querySelector('button .loading-container span').classList).toContain('spinner');
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
index 7da27bb8890..96e512d222a 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
@@ -20,7 +20,7 @@ describe('MRWidgetChecking', () => {
});
it('renders loading icon', () => {
- expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner');
+ expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('spinner');
});
it('renders information about merging', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index 0ddbdf67d8b..39b879612ae 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
-import { removeBreakLine } from 'spec/helpers/vue_component_helper';
+import { removeBreakLine } from 'spec/helpers/text_helper';
describe('MRWidgetConflicts', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index 3229ddd5e27..780bed1bf69 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -120,7 +120,7 @@ describe('MRWidgetFailedToMerge', () => {
it('renders given error', () => {
expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual(
- 'Merge error happened.',
+ 'Merge error happened',
);
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
index b9718a78fa4..8e0415b813b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
@@ -21,7 +21,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
canCancelAutomaticMerge: true,
mergeUserId: 1,
currentUserId: 1,
- setToMWPSBy: {},
+ setToAutoMergeBy: {},
sha,
targetBranchPath,
targetBranch,
@@ -106,7 +106,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(vm.service.merge).toHaveBeenCalledWith({
sha,
- merge_when_pipeline_succeeds: true,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
should_remove_source_branch: true,
});
done();
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index 477041fa383..1d2f3e41509 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { removeBreakLine } from 'spec/helpers/vue_component_helper';
+import { removeBreakLine } from 'spec/helpers/text_helper';
describe('MRWidgetPipelineBlocked', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
index f7523a01963..3e4ce2c3696 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
-import { removeBreakLine } from 'spec/helpers/vue_component_helper';
+import { removeBreakLine } from 'spec/helpers/text_helper';
describe('PipelineFailed', () => {
describe('template', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
index 631da202d1d..3ae773b6ccb 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -18,6 +18,7 @@ const createTestMr = customConfig => {
isPipelinePassing: false,
isMergeAllowed: true,
onlyAllowMergeIfPipelineSucceeds: false,
+ ffOnlyEnabled: false,
hasCI: false,
ciStatus: null,
sha: '12345678',
@@ -79,7 +80,7 @@ describe('ReadyToMerge', () => {
it('should have default data', () => {
expect(vm.mergeWhenBuildSucceeds).toBeFalsy();
expect(vm.useCommitMessageWithDescription).toBeFalsy();
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.showCommitMessageEditor).toBeFalsy();
expect(vm.isMakingRequest).toBeFalsy();
expect(vm.isMergingImmediately).toBeFalsy();
@@ -90,17 +91,17 @@ describe('ReadyToMerge', () => {
});
describe('computed', () => {
- describe('shouldShowMergeWhenPipelineSucceedsText', () => {
+ describe('shouldShowAutoMergeText', () => {
it('should return true with active pipeline', () => {
vm.mr.isPipelineActive = true;
- expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeTruthy();
+ expect(vm.shouldShowAutoMergeText).toBeTruthy();
});
it('should return false with inactive pipeline', () => {
vm.mr.isPipelineActive = false;
- expect(vm.shouldShowMergeWhenPipelineSucceedsText).toBeFalsy();
+ expect(vm.shouldShowAutoMergeText).toBeFalsy();
});
});
@@ -324,16 +325,20 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(true);
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeTruthy();
+ expect(vm.autoMergeStrategy).toBe('merge_when_pipeline_succeeds');
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
const params = vm.service.merge.calls.argsFor(0)[0];
- expect(params.sha).toEqual(vm.mr.sha);
- expect(params.commit_message).toEqual(vm.mr.commitMessage);
- expect(params.should_remove_source_branch).toBeFalsy();
- expect(params.merge_when_pipeline_succeeds).toBeTruthy();
+ expect(params).toEqual(
+ jasmine.objectContaining({
+ sha: vm.mr.sha,
+ commit_message: vm.mr.commitMessage,
+ should_remove_source_branch: false,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
+ }),
+ );
done();
}, 333);
});
@@ -344,7 +349,7 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick(false, true);
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.isMakingRequest).toBeTruthy();
expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
@@ -362,7 +367,7 @@ describe('ReadyToMerge', () => {
vm.handleMergeButtonClick();
setTimeout(() => {
- expect(vm.setToMergeWhenPipelineSucceeds).toBeFalsy();
+ expect(vm.autoMergeStrategy).toBeUndefined();
expect(vm.isMakingRequest).toBeTruthy();
expect(vm.initiateMergePolling).toHaveBeenCalled();
@@ -376,11 +381,29 @@ describe('ReadyToMerge', () => {
});
describe('initiateMergePolling', () => {
+ beforeEach(() => {
+ jasmine.clock().install();
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ });
+
it('should call simplePoll', () => {
const simplePoll = spyOnDependency(ReadyToMerge, 'simplePoll');
vm.initiateMergePolling();
- expect(simplePoll).toHaveBeenCalled();
+ expect(simplePoll).toHaveBeenCalledWith(jasmine.any(Function), { timeout: 0 });
+ });
+
+ it('should call handleMergePolling', () => {
+ spyOn(vm, 'handleMergePolling');
+
+ vm.initiateMergePolling();
+
+ jasmine.clock().tick(2000);
+
+ expect(vm.handleMergePolling).toHaveBeenCalled();
});
});
@@ -396,7 +419,7 @@ describe('ReadyToMerge', () => {
});
beforeEach(() => {
- loadFixtures('merge_requests/merge_request_of_current_user.html.raw');
+ loadFixtures('merge_requests/merge_request_of_current_user.html');
});
it('should call start and stop polling when MR merged', done => {
@@ -624,6 +647,10 @@ describe('ReadyToMerge', () => {
const findCommitsHeaderElement = () => wrapper.find(CommitsHeader);
const findCommitEditElements = () => wrapper.findAll(CommitEdit);
const findCommitDropdownElement = () => wrapper.find(CommitMessageDropdown);
+ const findFirstCommitEditLabel = () =>
+ findCommitEditElements()
+ .at(0)
+ .props('label');
describe('squash checkbox', () => {
it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => {
@@ -648,32 +675,130 @@ describe('ReadyToMerge', () => {
});
describe('commits count collapsible header', () => {
- it('should be rendered if fast-forward is disabled', () => {
+ it('should be rendered when fast-forward is disabled', () => {
createLocalComponent();
expect(findCommitsHeaderElement().exists()).toBeTruthy();
});
- it('should not be rendered if fast-forward is enabled', () => {
- createLocalComponent({ mr: { ffOnlyEnabled: true } });
+ describe('when fast-forward is enabled', () => {
+ it('should be rendered if squash and squash before are enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ enableSquashBeforeMerge: true,
+ squash: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeTruthy();
+ });
+
+ it('should not be rendered if squash before merge is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ enableSquashBeforeMerge: false,
+ squash: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
+
+ it('should not be rendered if squash is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: false,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
+
+ it('should not be rendered if commits count is 1', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 1,
+ },
+ });
- expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ expect(findCommitsHeaderElement().exists()).toBeFalsy();
+ });
});
});
describe('commits edit components', () => {
+ describe('when fast-forward merge is enabled', () => {
+ it('should not be rendered if squash is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: false,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should not be rendered if squash before merge is disabled', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: false,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should not be rendered if there is only one commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 1,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(0);
+ });
+
+ it('should have one edit component if squash is enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: {
+ ffOnlyEnabled: true,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ commitsCount: 2,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(1);
+ expect(findFirstCommitEditLabel()).toBe('Squash commit message');
+ });
+ });
+
it('should have one edit component when squash is disabled', () => {
createLocalComponent();
expect(findCommitEditElements().length).toBe(1);
});
- const findFirstCommitEditLabel = () =>
- findCommitEditElements()
- .at(0)
- .props('label');
-
- it('should have two edit components when squash is enabled', () => {
+ it('should have two edit components when squash is enabled and there is more than 1 commit', () => {
createLocalComponent({
mr: {
commitsCount: 2,
@@ -685,6 +810,18 @@ describe('ReadyToMerge', () => {
expect(findCommitEditElements().length).toBe(2);
});
+ it('should have one edit components when squash is enabled and there is 1 commit only', () => {
+ createLocalComponent({
+ mr: {
+ commitsCount: 1,
+ squash: true,
+ enableSquashBeforeMerge: true,
+ },
+ });
+
+ expect(findCommitEditElements().length).toBe(1);
+ });
+
it('should have correct edit merge commit label', () => {
createLocalComponent();
@@ -711,8 +848,10 @@ describe('ReadyToMerge', () => {
expect(findCommitDropdownElement().exists()).toBeFalsy();
});
- it('should be rendered if squash is enabled', () => {
- createLocalComponent({ mr: { squash: true } });
+ it('should be rendered if squash is enabled and there is more than 1 commit', () => {
+ createLocalComponent({
+ mr: { enableSquashBeforeMerge: true, squash: true, commitsCount: 2 },
+ });
expect(findCommitDropdownElement().exists()).toBeTruthy();
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
index 36f8c7a9683..9324c83bf4b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { removeBreakLine } from 'spec/helpers/vue_component_helper';
+import { removeBreakLine } from 'spec/helpers/text_helper';
describe('ShaMismatch', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 6ef07f81705..edbd0d54151 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -62,6 +62,7 @@ export default {
"Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
pipeline: {
id: 172,
+ iid: 32,
user: {
name: 'Administrator',
username: 'root',
@@ -134,6 +135,8 @@ export default {
yaml_errors: false,
retryable: true,
cancelable: false,
+ merge_request_pipeline: false,
+ detached_merge_request_pipeline: true,
},
ref: {
name: 'daaaa',
@@ -141,6 +144,15 @@ export default {
tag: false,
branch: true,
},
+ merge_request: {
+ iid: 1,
+ path: '/root/detached-merge-request-pipelines/merge_requests/1',
+ title: 'Update README.md',
+ source_branch: 'feature-1',
+ source_branch_path: '/root/detached-merge-request-pipelines/branches/feature-1',
+ target_branch: 'master',
+ target_branch_path: '/root/detached-merge-request-pipelines/branches/master',
+ },
commit: {
id: '104096c51715e12e7ae41f9333e9fa35b73f385d',
short_id: '104096c5',
@@ -222,12 +234,50 @@ export default {
merge_commit_path:
'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775',
troubleshooting_docs_path: 'help',
+ merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md',
squash: true,
+ visual_review_app_available: true,
};
export const mockStore = {
- pipeline: { id: 0 },
- mergePipeline: { id: 1 },
+ pipeline: {
+ id: 0,
+ iid: 0,
+ path: '/root/acets-app/pipelines/0',
+ details: {
+ status: {
+ details_path: '/root/review-app-tester/pipelines/66',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2. png',
+ group: 'success-with-warnings',
+ has_details: true,
+ icon: 'status_warning',
+ illustration: null,
+ label: 'passed with warnings',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ },
+ },
+ mergePipeline: {
+ id: 1,
+ iid: 1,
+ path: '/root/acets-app/pipelines/0',
+ details: {
+ status: {
+ details_path: '/root/review-app-tester/pipelines/66',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2. png',
+ group: 'success-with-warnings',
+ has_details: true,
+ icon: 'status_warning',
+ illustration: null,
+ label: 'passed with warnings',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ },
+ },
targetBranch: 'target-branch',
sourceBranch: 'source-branch',
sourceBranchLink: 'source-branch-link',
diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
index 3e8f73646c8..918717c4547 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -21,7 +21,6 @@ describe('mrWidgetOptions', () => {
const COLLABORATION_MESSAGE = 'Allows commits from members who can merge to the target branch';
beforeEach(() => {
- gon.features = { approvalRules: false };
// Prevent component mounting
delete mrWidgetOptions.el;
@@ -32,7 +31,6 @@ describe('mrWidgetOptions', () => {
});
afterEach(() => {
- gon.features = null;
vm.$destroy();
});
@@ -183,6 +181,101 @@ describe('mrWidgetOptions', () => {
});
});
});
+
+ describe('showMergePipelineForkWarning', () => {
+ describe('when the source project and target project are the same', () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'mergePipelinesEnabled', true);
+ Vue.set(vm.mr, 'sourceProjectId', 1);
+ Vue.set(vm.mr, 'targetProjectId', 1);
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showMergePipelineForkWarning).toEqual(false);
+ });
+ });
+
+ describe('when merge pipelines are not enabled', () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'mergePipelinesEnabled', false);
+ Vue.set(vm.mr, 'sourceProjectId', 1);
+ Vue.set(vm.mr, 'targetProjectId', 2);
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showMergePipelineForkWarning).toEqual(false);
+ });
+ });
+
+ describe('when merge pipelines are enabled _and_ the source project and target project are different', () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'mergePipelinesEnabled', true);
+ Vue.set(vm.mr, 'sourceProjectId', 1);
+ Vue.set(vm.mr, 'targetProjectId', 2);
+ vm.$nextTick(done);
+ });
+
+ it('should be true', () => {
+ expect(vm.showMergePipelineForkWarning).toEqual(true);
+ });
+ });
+ });
+
+ describe('showTargetBranchAdvancedError', () => {
+ describe(`when the pipeline's target_sha property doesn't exist`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
+ Vue.set(vm.mr.pipeline, 'target_sha', undefined);
+ Vue.set(vm.mr, 'targetBranchSha', 'abcd');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
+ describe(`when the pipeline's target_sha matches the target branch's sha`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'abcd');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
+ describe(`when the merge request is not open`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', false);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'bcde');
+ vm.$nextTick(done);
+ });
+
+ it('should be false', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(false);
+ });
+ });
+
+ describe(`when the pipeline's target_sha does not match the target branch's sha`, () => {
+ beforeEach(done => {
+ Vue.set(vm.mr, 'isOpen', true);
+ Vue.set(vm.mr.pipeline, 'target_sha', 'abcd');
+ Vue.set(vm.mr, 'targetBranchSha', 'bcde');
+ vm.$nextTick(done);
+ });
+
+ it('should be true', () => {
+ expect(vm.showTargetBranchAdvancedError).toEqual(true);
+ });
+ });
+ });
});
describe('methods', () => {
@@ -505,6 +598,7 @@ describe('mrWidgetOptions', () => {
];
const deploymentMockData = {
id: 15,
+ iid: 7,
name: 'review/diplo',
url: '/root/acets-review-apps/environments/15',
stop_url: '/root/acets-review-apps/environments/15/stop',
@@ -551,6 +645,7 @@ describe('mrWidgetOptions', () => {
vm.mr.state = 'merged';
vm.mr.mergePipeline = {
id: 127,
+ iid: 35,
user: {
id: 1,
name: 'Administrator',
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
index c226704694c..e2cd0f084fd 100644
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -36,8 +36,8 @@ describe('MergeRequestStore', () => {
expect(store.isPipelinePassing).toBe(true);
});
- it('is true when the CI status is `success_with_warnings`', () => {
- store.setData({ ...mockData, ci_status: 'success_with_warnings' });
+ it('is true when the CI status is `success-with-warnings`', () => {
+ store.setData({ ...mockData, ci_status: 'success-with-warnings' });
expect(store.isPipelinePassing).toBe(true);
});
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
index 4b0b7ba66e5..42481f8c334 100644
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -59,7 +59,7 @@ describe('CI Badge Link Component', () => {
success_warining: {
text: 'passed',
label: 'passed',
- group: 'success_with_warnings',
+ group: 'success-with-warnings',
icon: 'status_warning',
details_path: 'status/warning',
},
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 18fcdf7ede1..f2e20f626b5 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -61,7 +61,7 @@ describe('Commit component', () => {
});
it('should render a tag icon if it represents a tag', () => {
- expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag');
+ expect(component.$el.querySelector('.icon-container svg.ic-tag')).not.toBeNull();
});
it('should render a link to the ref url', () => {
@@ -143,4 +143,92 @@ describe('Commit component', () => {
);
});
});
+
+ describe('When commit ref is provided, but merge ref is not', () => {
+ it('should render the commit ref', () => {
+ props = {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ shortSha: 'b7836edd',
+ title: null,
+ author: {},
+ };
+
+ component = mountComponent(CommitComponent, props);
+ const refEl = component.$el.querySelector('.ref-name');
+
+ expect(refEl.textContent).toContain('master');
+
+ expect(refEl.href).toBe(props.commitRef.ref_url);
+
+ expect(refEl.getAttribute('data-original-title')).toBe(props.commitRef.name);
+
+ expect(component.$el.querySelector('.icon-container .ic-branch')).not.toBeNull();
+ });
+ });
+
+ describe('When both commit and merge ref are provided', () => {
+ it('should render the merge ref', () => {
+ props = {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ mergeRequestRef: {
+ iid: 1234,
+ path: 'https://example.com/path/to/mr',
+ title: 'Test MR',
+ },
+ shortSha: 'b7836edd',
+ title: null,
+ author: {},
+ };
+
+ component = mountComponent(CommitComponent, props);
+ const refEl = component.$el.querySelector('.ref-name');
+
+ expect(refEl.textContent).toContain('1234');
+
+ expect(refEl.href).toBe(props.mergeRequestRef.path);
+
+ expect(refEl.getAttribute('data-original-title')).toBe(props.mergeRequestRef.title);
+
+ expect(component.$el.querySelector('.icon-container .ic-git-merge')).not.toBeNull();
+ });
+ });
+
+ describe('When showRefInfo === false', () => {
+ it('should not render any ref info', () => {
+ props = {
+ tag: false,
+ commitRef: {
+ name: 'master',
+ ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
+ },
+ commitUrl:
+ 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
+ mergeRequestRef: {
+ iid: 1234,
+ path: '/path/to/mr',
+ title: 'Test MR',
+ },
+ shortSha: 'b7836edd',
+ title: null,
+ author: {},
+ showRefInfo: false,
+ };
+
+ component = mountComponent(CommitComponent, props);
+
+ expect(component.$el.querySelector('.ref-name')).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
index 4da8c6196b1..bdf802052b9 100644
--- a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { GREEN_BOX_IMAGE_URL } from 'spec/test_constants';
+import '~/behaviors/markdown/render_gfm';
describe('ContentViewer', () => {
let vm;
@@ -29,6 +30,7 @@ describe('ContentViewer', () => {
path: 'test.md',
content: '* Test',
projectPath: 'testproject',
+ type: 'markdown',
});
const previewContainer = vm.$el.querySelector('.md-previewer');
@@ -44,6 +46,7 @@ describe('ContentViewer', () => {
createComponent({
path: GREEN_BOX_IMAGE_URL,
fileSize: 1024,
+ type: 'image',
});
setTimeout(() => {
diff --git a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
index 7f2e246d656..97c870f27d9 100644
--- a/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
+++ b/spec/javascripts/vue_shared/components/diff_viewer/viewers/image_diff_viewer_spec.js
@@ -138,22 +138,6 @@ describe('ImageDiffViewer', () => {
done();
});
});
-
- it('drag handler is working', done => {
- vm.$el.querySelector('.view-modes-menu li:nth-child(2)').click();
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('1px');
- expect(vm.$el.querySelector('.top-handle')).not.toBeNull();
-
- dragSlider(vm.$el.querySelector('.swipe-bar'), 40);
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.swipe-bar').style.left).toBe('-20px');
- done();
- });
- });
- });
});
describe('onionSkin', () => {
diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js
index 34c9b35e02a..5bea8c43da3 100644
--- a/spec/javascripts/vue_shared/components/file_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/file_icon_spec.js
@@ -70,12 +70,9 @@ describe('File Icon component', () => {
loading: true,
});
- const { classList } = vm.$el.querySelector('i');
+ const { classList } = vm.$el.querySelector('.loading-container span');
- expect(classList.contains('fa')).toEqual(true);
- expect(classList.contains('fa-spin')).toEqual(true);
- expect(classList.contains('fa-spinner')).toEqual(true);
- expect(classList.contains('fa-1x')).toEqual(true);
+ expect(classList.contains('spinner')).toEqual(true);
});
it('should add a special class and a size class', () => {
diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js
index d1fd899c1a8..7da69e3fa84 100644
--- a/spec/javascripts/vue_shared/components/file_row_spec.js
+++ b/spec/javascripts/vue_shared/components/file_row_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import FileRow from '~/vue_shared/components/file_row.vue';
+import FileRowExtra from '~/ide/components/file_row_extra.vue';
import { file } from 'spec/ide/helpers';
import mountComponent from '../../helpers/vue_mount_component_helper';
@@ -16,6 +17,10 @@ describe('File row component', () => {
vm.$destroy();
});
+ const findNewDropdown = () => vm.$el.querySelector('.ide-new-btn .dropdown');
+ const findNewDropdownButton = () => vm.$el.querySelector('.ide-new-btn .dropdown button');
+ const findFileRow = () => vm.$el.querySelector('.file-row');
+
it('renders name', () => {
createComponent({
file: file('t4'),
@@ -84,4 +89,59 @@ describe('File row component', () => {
expect(vm.$el.querySelector('.js-file-row-header')).not.toBe(null);
});
+
+ describe('new dropdown', () => {
+ beforeEach(() => {
+ createComponent({
+ file: file('t5'),
+ level: 1,
+ extraComponent: FileRowExtra,
+ });
+ });
+
+ it('renders in extra component', () => {
+ expect(findNewDropdown()).not.toBe(null);
+ });
+
+ it('is hidden at start', () => {
+ expect(findNewDropdown()).not.toHaveClass('show');
+ });
+
+ it('is opened when button is clicked', done => {
+ expect(vm.dropdownOpen).toBe(false);
+ findNewDropdownButton().dispatchEvent(new Event('click'));
+
+ vm.$nextTick()
+ .then(() => {
+ expect(vm.dropdownOpen).toBe(true);
+ expect(findNewDropdown()).toHaveClass('show');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('when opened', () => {
+ beforeEach(() => {
+ vm.dropdownOpen = true;
+ });
+
+ it('stays open when button triggers mouseout', () => {
+ findNewDropdownButton().dispatchEvent(new Event('mouseout'));
+
+ expect(vm.dropdownOpen).toBe(true);
+ });
+
+ it('stays open when button triggers mouseleave', () => {
+ findNewDropdownButton().dispatchEvent(new Event('mouseleave'));
+
+ expect(vm.dropdownOpen).toBe(true);
+ });
+
+ it('closes when row triggers mouseleave', () => {
+ findFileRow().dispatchEvent(new Event('mouseleave'));
+
+ expect(vm.dropdownOpen).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index 7a741bdc067..a9c1a67b39b 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -88,7 +88,7 @@ describe('Header CI Component', () => {
vm.actions[0].isLoading = true;
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toBeFalsy();
+ expect(vm.$el.querySelector('.btn .spinner').getAttribute('style')).toBeFalsy();
done();
});
});
diff --git a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js b/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
deleted file mode 100644
index 8fca2637326..00000000000
--- a/spec/javascripts/vue_shared/components/issue/issue_milestone_spec.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import Vue from 'vue';
-
-import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { mockMilestone } from 'spec/boards/mock_data';
-
-const createComponent = (milestone = mockMilestone) => {
- const Component = Vue.extend(IssueMilestone);
-
- return mountComponent(Component, {
- milestone,
- });
-};
-
-describe('IssueMilestoneComponent', () => {
- let vm;
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('computed', () => {
- describe('isMilestoneStarted', () => {
- it('should return `false` when milestoneStart prop is not defined', done => {
- const vmStartUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStartUndefined.isMilestoneStarted).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmStartUndefined.$destroy();
- });
-
- it('should return `true` when milestone start date is past current date', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.isMilestoneStarted).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
- });
-
- describe('isMilestonePastDue', () => {
- it('should return `false` when milestoneDue prop is not defined', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.isMilestonePastDue).toBe(false);
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('should return `true` when milestone due is past current date', done => {
- const vmPastDue = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '1990-07-22',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmPastDue.isMilestonePastDue).toBe(true);
- })
- .then(done)
- .catch(done.fail);
-
- vmPastDue.$destroy();
- });
- });
-
- describe('milestoneDatesAbsolute', () => {
- it('returns string containing absolute milestone due date', () => {
- expect(vm.milestoneDatesAbsolute).toBe('(December 31, 2019)');
- });
-
- it('returns string containing absolute milestone start date when due date is not present', done => {
- const vmDueUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDueUndefined.milestoneDatesAbsolute).toBe('(January 1, 2018)');
- })
- .then(done)
- .catch(done.fail);
-
- vmDueUndefined.$destroy();
- });
-
- it('returns empty string when both milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesAbsolute).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
-
- describe('milestoneDatesHuman', () => {
- it('returns string containing milestone due date when date is yet to be due', done => {
- const vmFuture = createComponent(
- Object.assign({}, mockMilestone, {
- due_date: `${new Date().getFullYear() + 10}-01-01`,
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmFuture.milestoneDatesHuman).toContain('years remaining');
- })
- .then(done)
- .catch(done.fail);
-
- vmFuture.$destroy();
- });
-
- it('returns string containing milestone start date when date has already started and due date is not present', done => {
- const vmStarted = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '1990-07-22',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarted.milestoneDatesHuman).toContain('Started');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarted.$destroy();
- });
-
- it('returns string containing milestone start date when date is yet to start and due date is not present', done => {
- const vmStarts = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: `${new Date().getFullYear() + 10}-01-01`,
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmStarts.milestoneDatesHuman).toContain('Starts');
- })
- .then(done)
- .catch(done.fail);
-
- vmStarts.$destroy();
- });
-
- it('returns empty string when milestone start and due dates are not present', done => {
- const vmDatesUndefined = createComponent(
- Object.assign({}, mockMilestone, {
- start_date: '',
- due_date: '',
- }),
- );
-
- Vue.nextTick()
- .then(() => {
- expect(vmDatesUndefined.milestoneDatesHuman).toBe('');
- })
- .then(done)
- .catch(done.fail);
-
- vmDatesUndefined.$destroy();
- });
- });
- });
-
- describe('template', () => {
- it('renders component root element with class `issue-milestone-details`', () => {
- expect(vm.$el.classList.contains('issue-milestone-details')).toBe(true);
- });
-
- it('renders milestone icon', () => {
- expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('clock');
- });
-
- it('renders milestone title', () => {
- expect(vm.$el.querySelector('.milestone-title').innerText.trim()).toBe(mockMilestone.title);
- });
-
- it('renders milestone tooltip', () => {
- expect(vm.$el.querySelector('.js-item-milestone').innerText.trim()).toContain(
- mockMilestone.title,
- );
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/issue/related_issuable_mock_data.js b/spec/javascripts/vue_shared/components/issue/related_issuable_mock_data.js
new file mode 100644
index 00000000000..26bfdd7551e
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/issue/related_issuable_mock_data.js
@@ -0,0 +1,111 @@
+export const defaultProps = {
+ endpoint: '/foo/bar/issues/1/related_issues',
+ currentNamespacePath: 'foo',
+ currentProjectPath: 'bar',
+};
+
+export const issuable1 = {
+ id: 200,
+ epic_issue_id: 1,
+ confidential: false,
+ reference: 'foo/bar#123',
+ displayReference: '#123',
+ title: 'some title',
+ path: '/foo/bar/issues/123',
+ state: 'opened',
+};
+
+export const issuable2 = {
+ id: 201,
+ epic_issue_id: 2,
+ confidential: false,
+ reference: 'foo/bar#124',
+ displayReference: '#124',
+ title: 'some other thing',
+ path: '/foo/bar/issues/124',
+ state: 'opened',
+};
+
+export const issuable3 = {
+ id: 202,
+ epic_issue_id: 3,
+ confidential: false,
+ reference: 'foo/bar#125',
+ displayReference: '#125',
+ title: 'some other other thing',
+ path: '/foo/bar/issues/125',
+ state: 'opened',
+};
+
+export const issuable4 = {
+ id: 203,
+ epic_issue_id: 4,
+ confidential: false,
+ reference: 'foo/bar#126',
+ displayReference: '#126',
+ title: 'some other other other thing',
+ path: '/foo/bar/issues/126',
+ state: 'opened',
+};
+
+export const issuable5 = {
+ id: 204,
+ epic_issue_id: 5,
+ confidential: false,
+ reference: 'foo/bar#127',
+ displayReference: '#127',
+ title: 'some other other other thing',
+ path: '/foo/bar/issues/127',
+ state: 'opened',
+};
+
+export const defaultMilestone = {
+ id: 1,
+ state: 'active',
+ title: 'Milestone title',
+ start_date: '2018-01-01',
+ due_date: '2019-12-31',
+};
+
+export const defaultAssignees = [
+ {
+ id: 1,
+ name: 'Administrator',
+ username: 'root',
+ state: 'active',
+ avatar_url: `${gl.TEST_HOST}`,
+ web_url: `${gl.TEST_HOST}/root`,
+ status_tooltip_html: null,
+ path: '/root',
+ },
+ {
+ id: 13,
+ name: 'Brooks Beatty',
+ username: 'brynn_champlin',
+ state: 'active',
+ avatar_url: `${gl.TEST_HOST}`,
+ web_url: `${gl.TEST_HOST}/brynn_champlin`,
+ status_tooltip_html: null,
+ path: '/brynn_champlin',
+ },
+ {
+ id: 6,
+ name: 'Bryce Turcotte',
+ username: 'melynda',
+ state: 'active',
+ avatar_url: `${gl.TEST_HOST}`,
+ web_url: `${gl.TEST_HOST}/melynda`,
+ status_tooltip_html: null,
+ path: '/melynda',
+ },
+ {
+ id: 20,
+ name: 'Conchita Eichmann',
+ username: 'juliana_gulgowski',
+ state: 'active',
+ avatar_url: `${gl.TEST_HOST}`,
+ web_url: `${gl.TEST_HOST}/juliana_gulgowski`,
+ status_tooltip_html: null,
+ path: '/juliana_gulgowski',
+ },
+];
diff --git a/spec/javascripts/vue_shared/components/markdown/field_spec.js b/spec/javascripts/vue_shared/components/markdown/field_spec.js
index 79e0e756a7a..02d73e1849a 100644
--- a/spec/javascripts/vue_shared/components/markdown/field_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/field_spec.js
@@ -5,7 +5,7 @@ import fieldComponent from '~/vue_shared/components/markdown/field.vue';
function assertMarkdownTabs(isWrite, writeLink, previewLink, vm) {
expect(writeLink.parentNode.classList.contains('active')).toEqual(isWrite);
expect(previewLink.parentNode.classList.contains('active')).toEqual(!isWrite);
- expect(vm.$el.querySelector('.md-preview').style.display).toEqual(isWrite ? 'none' : '');
+ expect(vm.$el.querySelector('.md-preview-holder').style.display).toEqual(isWrite ? 'none' : '');
}
describe('Markdown field component', () => {
@@ -80,7 +80,9 @@ describe('Markdown field component', () => {
previewLink.click();
Vue.nextTick(() => {
- expect(vm.$el.querySelector('.md-preview').textContent.trim()).toContain('Loading…');
+ expect(vm.$el.querySelector('.md-preview-holder').textContent.trim()).toContain(
+ 'Loading…',
+ );
done();
});
@@ -90,7 +92,7 @@ describe('Markdown field component', () => {
previewLink.click();
setTimeout(() => {
- expect(vm.$el.querySelector('.md-preview').innerHTML).toContain(
+ expect(vm.$el.querySelector('.md-preview-holder').innerHTML).toContain(
'<p>markdown preview</p>',
);
diff --git a/spec/javascripts/vue_shared/components/markdown/header_spec.js b/spec/javascripts/vue_shared/components/markdown/header_spec.js
index e733a95288e..d4be2451f0b 100644
--- a/spec/javascripts/vue_shared/components/markdown/header_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/header_spec.js
@@ -98,7 +98,7 @@ describe('Markdown field header component', () => {
it('renders suggestion template', () => {
vm.lineContent = 'Some content';
- expect(vm.mdSuggestion).toEqual('```suggestion\n{text}\n```');
+ expect(vm.mdSuggestion).toEqual('```suggestion:-0+0\n{text}\n```');
});
it('does not render suggestion button if `canSuggest` is set to false', () => {
diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js
deleted file mode 100644
index 12ee804f668..00000000000
--- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import Vue from 'vue';
-import SuggestionDiffHeaderComponent from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
-
-const MOCK_DATA = {
- canApply: true,
- isApplied: false,
- helpPagePath: 'path_to_docs',
-};
-
-describe('Suggestion Diff component', () => {
- let vm;
-
- function createComponent(propsData) {
- const Component = Vue.extend(SuggestionDiffHeaderComponent);
-
- return new Component({
- propsData,
- }).$mount();
- }
-
- beforeEach(done => {
- vm = createComponent(MOCK_DATA);
- Vue.nextTick(done);
- });
-
- describe('init', () => {
- it('renders a suggestion header', () => {
- const header = vm.$el.querySelector('.qa-suggestion-diff-header');
-
- expect(header).not.toBeNull();
- expect(header.innerHTML.includes('Suggested change')).toBe(true);
- });
-
- it('renders a help button', () => {
- const helpBtn = vm.$el.querySelector('.js-help-btn');
-
- expect(helpBtn).not.toBeNull();
- });
-
- it('renders an apply button', () => {
- const applyBtn = vm.$el.querySelector('.qa-apply-btn');
-
- expect(applyBtn).not.toBeNull();
- expect(applyBtn.innerHTML.includes('Apply suggestion')).toBe(true);
- });
-
- it('does not render an apply button if `canApply` is set to false', () => {
- const props = Object.assign(MOCK_DATA, { canApply: false });
-
- vm = createComponent(props);
-
- expect(vm.$el.querySelector('.qa-apply-btn')).toBeNull();
- });
- });
-
- describe('applySuggestion', () => {
- it('emits when the apply button is clicked', () => {
- const props = Object.assign(MOCK_DATA, { canApply: true });
-
- vm = createComponent(props);
- spyOn(vm, '$emit');
- vm.applySuggestion();
-
- expect(vm.$emit).toHaveBeenCalled();
- });
-
- it('does not emit when the canApply is set to false', () => {
- spyOn(vm, '$emit');
- vm.canApply = false;
- vm.applySuggestion();
-
- expect(vm.$emit).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js
index f87c2a92f47..ea74cb9eb21 100644
--- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_spec.js
@@ -1,21 +1,50 @@
import Vue from 'vue';
import SuggestionDiffComponent from '~/vue_shared/components/markdown/suggestion_diff.vue';
+import { selectDiffLines } from '~/vue_shared/components/lib/utils/diff_utils';
const MOCK_DATA = {
canApply: true,
- newLines: [
- { content: 'Line 1\n', lineNumber: 1 },
- { content: 'Line 2\n', lineNumber: 2 },
- { content: 'Line 3\n', lineNumber: 3 },
- ],
- fromLine: 1,
- fromContent: 'Old content',
suggestion: {
id: 1,
+ diff_lines: [
+ {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: null,
+ old_line: 5,
+ rich_text: '-test',
+ text: '-test',
+ type: 'old',
+ },
+ {
+ can_receive_suggestion: true,
+ line_code: null,
+ meta_data: null,
+ new_line: 5,
+ old_line: null,
+ rich_text: '+new test',
+ text: '+new test',
+ type: 'new',
+ },
+ {
+ can_receive_suggestion: true,
+ line_code: null,
+ meta_data: null,
+ new_line: 5,
+ old_line: null,
+ rich_text: '+new test2',
+ text: '+new test2',
+ type: 'new',
+ },
+ ],
},
helpPagePath: 'path_to_docs',
};
+const lines = selectDiffLines(MOCK_DATA.suggestion.diff_lines);
+const newLines = lines.filter(line => line.type === 'new');
+
describe('Suggestion Diff component', () => {
let vm;
@@ -39,30 +68,23 @@ describe('Suggestion Diff component', () => {
});
it('renders the oldLineNumber', () => {
- const fromLine = vm.$el.querySelector('.qa-old-diff-line-number').innerHTML;
+ const fromLine = vm.$el.querySelector('.old_line').innerHTML;
- expect(parseInt(fromLine, 10)).toBe(vm.fromLine);
+ expect(parseInt(fromLine, 10)).toBe(lines[0].old_line);
});
it('renders the oldLineContent', () => {
const fromContent = vm.$el.querySelector('.line_content.old').innerHTML;
- expect(fromContent.includes(vm.fromContent)).toBe(true);
- });
-
- it('renders the contents of newLines', () => {
- const newLines = vm.$el.querySelectorAll('.line_holder.new');
-
- newLines.forEach((line, i) => {
- expect(newLines[i].innerHTML.includes(vm.newLines[i].content)).toBe(true);
- });
+ expect(fromContent.includes(lines[0].text)).toBe(true);
});
- it('renders a line number for each line', () => {
- const newLineNumbers = vm.$el.querySelectorAll('.qa-new-diff-line-number');
+ it('renders new lines', () => {
+ const newLinesElements = vm.$el.querySelectorAll('.line_holder.new');
- newLineNumbers.forEach((line, i) => {
- expect(newLineNumbers[i].innerHTML.includes(vm.newLines[i].lineNumber)).toBe(true);
+ newLinesElements.forEach((line, i) => {
+ expect(newLinesElements[i].innerHTML.includes(newLines[i].new_line)).toBe(true);
+ expect(newLinesElements[i].innerHTML.includes(newLines[i].text)).toBe(true);
});
});
});
diff --git a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js
index 33be63a3a1e..b7de40b4831 100644
--- a/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js
+++ b/spec/javascripts/vue_shared/components/markdown/suggestions_spec.js
@@ -2,46 +2,52 @@ import Vue from 'vue';
import SuggestionsComponent from '~/vue_shared/components/markdown/suggestions.vue';
const MOCK_DATA = {
- fromLine: 1,
- fromContent: 'Old content',
- suggestions: [],
+ suggestions: [
+ {
+ id: 1,
+ appliable: true,
+ applied: false,
+ current_user: {
+ can_apply: true,
+ },
+ diff_lines: [
+ {
+ can_receive_suggestion: false,
+ line_code: null,
+ meta_data: null,
+ new_line: null,
+ old_line: 5,
+ rich_text: '-test',
+ text: '-test',
+ type: 'old',
+ },
+ {
+ can_receive_suggestion: true,
+ line_code: null,
+ meta_data: null,
+ new_line: 5,
+ old_line: null,
+ rich_text: '+new test',
+ text: '+new test',
+ type: 'new',
+ },
+ ],
+ },
+ ],
noteHtml: `
+ <div class="suggestion">
+ <div class="line">-oldtest</div>
+ </div>
<div class="suggestion">
- <div class="line">Suggestion 1</div>
+ <div class="line">+newtest</div>
</div>
-
- <div class="suggestion">
- <div class="line">Suggestion 2</div>
- </div>
`,
isApplied: false,
helpPagePath: 'path_to_docs',
};
-const generateLine = content => {
- const line = document.createElement('div');
- line.className = 'line';
- line.innerHTML = content;
-
- return line;
-};
-
-const generateMockLines = () => {
- const line1 = generateLine('Line 1');
- const line2 = generateLine('Line 2');
- const line3 = generateLine('- Line 3');
- const container = document.createElement('div');
-
- container.appendChild(line1);
- container.appendChild(line2);
- container.appendChild(line3);
-
- return container;
-};
-
describe('Suggestion component', () => {
let vm;
- let extractedLines;
let diffTable;
beforeEach(done => {
@@ -51,8 +57,7 @@ describe('Suggestion component', () => {
propsData: MOCK_DATA,
}).$mount();
- extractedLines = vm.extractNewLines(generateMockLines());
- diffTable = vm.generateDiff(extractedLines).$mount().$el;
+ diffTable = vm.generateDiff(0).$mount().$el;
spyOn(vm, 'renderSuggestions');
vm.renderSuggestions();
@@ -70,32 +75,8 @@ describe('Suggestion component', () => {
it('renders suggestions', () => {
expect(vm.renderSuggestions).toHaveBeenCalled();
- expect(vm.$el.innerHTML.includes('Suggestion 1')).toBe(true);
- expect(vm.$el.innerHTML.includes('Suggestion 2')).toBe(true);
- });
- });
-
- describe('extractNewLines', () => {
- it('extracts suggested lines', () => {
- const expectedReturn = [
- { content: 'Line 1\n', lineNumber: 1 },
- { content: 'Line 2\n', lineNumber: 2 },
- { content: '- Line 3\n', lineNumber: 3 },
- ];
-
- expect(vm.extractNewLines(generateMockLines())).toEqual(expectedReturn);
- });
-
- it('increments line number for each extracted line', () => {
- expect(extractedLines[0].lineNumber).toEqual(1);
- expect(extractedLines[1].lineNumber).toEqual(2);
- expect(extractedLines[2].lineNumber).toEqual(3);
- });
-
- it('returns empty array if no lines are found', () => {
- const el = document.createElement('div');
-
- expect(vm.extractNewLines(el)).toEqual([]);
+ expect(vm.$el.innerHTML.includes('oldtest')).toBe(true);
+ expect(vm.$el.innerHTML.includes('newtest')).toBe(true);
});
});
@@ -109,17 +90,17 @@ describe('Suggestion component', () => {
});
it('generates a diff table that contains contents the suggested lines', () => {
- extractedLines.forEach((line, i) => {
- expect(diffTable.innerHTML.includes(extractedLines[i].content)).toBe(true);
+ MOCK_DATA.suggestions[0].diff_lines.forEach(line => {
+ const text = line.text.substring(1);
+
+ expect(diffTable.innerHTML.includes(text)).toBe(true);
});
});
it('generates a diff table with the correct line number for each suggested line', () => {
- const lines = diffTable.getElementsByClassName('qa-new-diff-line-number');
+ const lines = diffTable.querySelectorAll('.old_line');
- expect([...lines][0].innerHTML).toBe('1');
- expect([...lines][1].innerHTML).toBe('2');
- expect([...lines][2].innerHTML).toBe('3');
+ expect(parseInt([...lines][0].innerHTML, 10)).toBe(5);
});
});
});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
new file mode 100644
index 00000000000..47964a1702a
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/project_selector/project_list_item_spec.js
@@ -0,0 +1,110 @@
+import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { trimText } from 'spec/helpers/text_helper';
+
+const localVue = createLocalVue();
+
+describe('ProjectListItem component', () => {
+ const Component = localVue.extend(ProjectListItem);
+ let wrapper;
+ let vm;
+ let options;
+ loadJSONFixtures('static/projects.json');
+ const project = getJSONFixture('static/projects.json')[0];
+
+ beforeEach(() => {
+ options = {
+ propsData: {
+ project,
+ selected: false,
+ },
+ sync: false,
+ localVue,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.vm.$destroy();
+ });
+
+ it('does not render a check mark icon if selected === false', () => {
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-selected-icon.js-unselected')).toBe(true);
+ });
+
+ it('renders a check mark icon if selected === true', () => {
+ options.propsData.selected = true;
+
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-selected-icon.js-selected')).toBe(true);
+ });
+
+ it(`emits a "clicked" event when clicked`, () => {
+ wrapper = shallowMount(Component, options);
+ ({ vm } = wrapper);
+
+ spyOn(vm, '$emit');
+ wrapper.vm.onClick();
+
+ expect(wrapper.vm.$emit).toHaveBeenCalledWith('click');
+ });
+
+ it(`renders the project avatar`, () => {
+ wrapper = shallowMount(Component, options);
+
+ expect(wrapper.contains('.js-project-avatar')).toBe(true);
+ });
+
+ it(`renders a simple namespace name with a trailing slash`, () => {
+ options.propsData.project.name_with_namespace = 'a / b';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('a /');
+ });
+
+ it(`renders a properly truncated namespace with a trailing slash`, () => {
+ options.propsData.project.name_with_namespace = 'a / b / c / d / e / f';
+
+ wrapper = shallowMount(Component, options);
+ const renderedNamespace = trimText(wrapper.find('.js-project-namespace').text());
+
+ expect(renderedNamespace).toBe('a / ... / e /');
+ });
+
+ it(`renders the project name`, () => {
+ options.propsData.project.name = 'my-test-project';
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').text());
+
+ expect(renderedName).toBe('my-test-project');
+ });
+
+ it(`renders the project name with highlighting in the case of a search query match`, () => {
+ options.propsData.project.name = 'my-test-project';
+ options.propsData.matcher = 'pro';
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').html());
+ const expected = 'my-test-<b>p</b><b>r</b><b>o</b>ject';
+
+ expect(renderedName).toContain(expected);
+ });
+
+ it('prevents search query and project name XSS', () => {
+ const alertSpy = spyOn(window, 'alert');
+ options.propsData.project.name = "my-xss-pro<script>alert('XSS');</script>ject";
+ options.propsData.matcher = "pro<script>alert('XSS');</script>";
+
+ wrapper = shallowMount(Component, options);
+ const renderedName = trimText(wrapper.find('.js-project-name').html());
+ const expected = 'my-xss-project';
+
+ expect(renderedName).toContain(expected);
+ expect(alertSpy).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
new file mode 100644
index 00000000000..7f5f1a778d7
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/project_selector/project_selector_spec.js
@@ -0,0 +1,132 @@
+import Vue from 'vue';
+import _ from 'underscore';
+import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
+import ProjectListItem from '~/vue_shared/components/project_selector/project_list_item.vue';
+import { shallowMount } from '@vue/test-utils';
+import { trimText } from 'spec/helpers/text_helper';
+
+describe('ProjectSelector component', () => {
+ let wrapper;
+ let vm;
+ loadJSONFixtures('static/projects.json');
+ const allProjects = getJSONFixture('static/projects.json');
+ const searchResults = allProjects.slice(0, 5);
+ let selected = [];
+ selected = selected.concat(allProjects.slice(0, 3)).concat(allProjects.slice(5, 8));
+
+ beforeEach(() => {
+ jasmine.clock().install();
+
+ wrapper = shallowMount(Vue.extend(ProjectSelector), {
+ propsData: {
+ projectSearchResults: searchResults,
+ selectedProjects: selected,
+ showNoResultsMessage: false,
+ showMinimumSearchQueryMessage: false,
+ showLoadingIndicator: false,
+ showSearchErrorMessage: false,
+ },
+ attachToDocument: true,
+ });
+
+ ({ vm } = wrapper);
+ });
+
+ afterEach(() => {
+ jasmine.clock().uninstall();
+ vm.$destroy();
+ });
+
+ it('renders the search results', () => {
+ expect(wrapper.findAll('.js-project-list-item').length).toBe(5);
+ });
+
+ it(`triggers a (debounced) search when the search input value changes`, () => {
+ spyOn(vm, '$emit');
+ const query = 'my test query!';
+ const searchInput = wrapper.find('.js-project-selector-input');
+ searchInput.setValue(query);
+ searchInput.trigger('input');
+
+ expect(vm.$emit).not.toHaveBeenCalledWith();
+ jasmine.clock().tick(501);
+
+ expect(vm.$emit).toHaveBeenCalledWith('searched', query);
+ });
+
+ it(`debounces the search input`, () => {
+ spyOn(vm, '$emit');
+ const searchInput = wrapper.find('.js-project-selector-input');
+
+ const updateSearchQuery = (count = 0) => {
+ if (count === 10) {
+ jasmine.clock().tick(101);
+
+ expect(vm.$emit).toHaveBeenCalledTimes(1);
+ expect(vm.$emit).toHaveBeenCalledWith('searched', `search query #9`);
+ } else {
+ searchInput.setValue(`search query #${count}`);
+ searchInput.trigger('input');
+
+ jasmine.clock().tick(400);
+ updateSearchQuery(count + 1);
+ }
+ };
+
+ updateSearchQuery();
+ });
+
+ it(`includes a placeholder in the search box`, () => {
+ expect(wrapper.find('.js-project-selector-input').attributes('placeholder')).toBe(
+ 'Search your projects',
+ );
+ });
+
+ it(`triggers a "projectClicked" event when a project is clicked`, () => {
+ spyOn(vm, '$emit');
+ wrapper.find(ProjectListItem).vm.$emit('click', _.first(searchResults));
+
+ expect(vm.$emit).toHaveBeenCalledWith('projectClicked', _.first(searchResults));
+ });
+
+ it(`shows a "no results" message if showNoResultsMessage === true`, () => {
+ wrapper.setProps({ showNoResultsMessage: true });
+
+ expect(wrapper.contains('.js-no-results-message')).toBe(true);
+
+ const noResultsEl = wrapper.find('.js-no-results-message');
+
+ expect(trimText(noResultsEl.text())).toEqual('Sorry, no projects matched your search');
+ });
+
+ it(`shows a "minimum search query" message if showMinimumSearchQueryMessage === true`, () => {
+ wrapper.setProps({ showMinimumSearchQueryMessage: true });
+
+ expect(wrapper.contains('.js-minimum-search-query-message')).toBe(true);
+
+ const minimumSearchEl = wrapper.find('.js-minimum-search-query-message');
+
+ expect(trimText(minimumSearchEl.text())).toEqual('Enter at least three characters to search');
+ });
+
+ it(`shows a error message if showSearchErrorMessage === true`, () => {
+ wrapper.setProps({ showSearchErrorMessage: true });
+
+ expect(wrapper.contains('.js-search-error-message')).toBe(true);
+
+ const errorMessageEl = wrapper.find('.js-search-error-message');
+
+ expect(trimText(errorMessageEl.text())).toEqual(
+ 'Something went wrong, unable to search projects',
+ );
+ });
+
+ it(`focuses the input element when the focusSearchInput() method is called`, () => {
+ const input = wrapper.find('.js-project-selector-input');
+
+ expect(document.activeElement).not.toBe(input.element);
+ vm.focusSearchInput();
+
+ expect(document.activeElement).toBe(input.element);
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
index 3fcb91b6f5e..70025f041a7 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -6,6 +6,13 @@ export const mockLabels = [
color: '#BADA55',
text_color: '#FFFFFF',
},
+ {
+ id: 27,
+ title: 'Foo::Bar',
+ description: 'Foobar',
+ color: '#0033CC',
+ text_color: '#FFFFFF',
+ },
];
export const mockSuggestedColors = [
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
index 0dcb712e720..42cd41381dc 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -22,10 +22,10 @@ describe('Pagination component', () => {
it('should not render anything', () => {
component = mountComponent({
pageInfo: {
- nextPage: 1,
+ nextPage: NaN,
page: 1,
perPage: 20,
- previousPage: null,
+ previousPage: NaN,
total: 15,
totalPages: 1,
},
@@ -53,7 +53,29 @@ describe('Pagination component', () => {
component.$el.querySelector('.js-previous-button').classList.contains('disabled'),
).toEqual(true);
- component.$el.querySelector('.js-previous-button a').click();
+ component.$el.querySelector('.js-previous-button .page-link').click();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should be disabled and non clickable when total and totalPages are NaN', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: 2,
+ page: 1,
+ perPage: 20,
+ previousPage: NaN,
+ total: NaN,
+ totalPages: NaN,
+ },
+ change: spy,
+ });
+
+ expect(
+ component.$el.querySelector('.js-previous-button').classList.contains('disabled'),
+ ).toEqual(true);
+
+ component.$el.querySelector('.js-previous-button .page-link').click();
expect(spy).not.toHaveBeenCalled();
});
@@ -71,7 +93,25 @@ describe('Pagination component', () => {
change: spy,
});
- component.$el.querySelector('.js-previous-button a').click();
+ component.$el.querySelector('.js-previous-button .page-link').click();
+
+ expect(spy).toHaveBeenCalledWith(1);
+ });
+
+ it('should be enabled and clickable when total and totalPages are NaN', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: 3,
+ page: 2,
+ perPage: 20,
+ previousPage: 1,
+ total: NaN,
+ totalPages: NaN,
+ },
+ change: spy,
+ });
+
+ component.$el.querySelector('.js-previous-button .page-link').click();
expect(spy).toHaveBeenCalledWith(1);
});
@@ -91,7 +131,29 @@ describe('Pagination component', () => {
change: spy,
});
- const button = component.$el.querySelector('.js-first-button a');
+ const button = component.$el.querySelector('.js-first-button .page-link');
+
+ expect(button.textContent.trim()).toEqual('« First');
+
+ button.click();
+
+ expect(spy).toHaveBeenCalledWith(1);
+ });
+
+ it('should call the change callback with the first page when total and totalPages are NaN', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: 3,
+ page: 2,
+ perPage: 20,
+ previousPage: 1,
+ total: NaN,
+ totalPages: NaN,
+ },
+ change: spy,
+ });
+
+ const button = component.$el.querySelector('.js-first-button .page-link');
expect(button.textContent.trim()).toEqual('« First');
@@ -115,7 +177,7 @@ describe('Pagination component', () => {
change: spy,
});
- const button = component.$el.querySelector('.js-last-button a');
+ const button = component.$el.querySelector('.js-last-button .page-link');
expect(button.textContent.trim()).toEqual('Last »');
@@ -123,16 +185,32 @@ describe('Pagination component', () => {
expect(spy).toHaveBeenCalledWith(5);
});
+
+ it('should not render', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: 3,
+ page: 2,
+ perPage: 20,
+ previousPage: 1,
+ total: NaN,
+ totalPages: NaN,
+ },
+ change: spy,
+ });
+
+ expect(component.$el.querySelector('.js-last-button .page-link')).toBeNull();
+ });
});
describe('next button', () => {
it('should be disabled and non clickable', () => {
component = mountComponent({
pageInfo: {
- nextPage: 5,
+ nextPage: NaN,
page: 5,
perPage: 20,
- previousPage: 1,
+ previousPage: 4,
total: 84,
totalPages: 5,
},
@@ -141,7 +219,27 @@ describe('Pagination component', () => {
expect(component.$el.querySelector('.js-next-button').textContent.trim()).toEqual('Next');
- component.$el.querySelector('.js-next-button a').click();
+ component.$el.querySelector('.js-next-button .page-link').click();
+
+ expect(spy).not.toHaveBeenCalled();
+ });
+
+ it('should be disabled and non clickable when total and totalPages are NaN', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: NaN,
+ page: 5,
+ perPage: 20,
+ previousPage: 4,
+ total: NaN,
+ totalPages: NaN,
+ },
+ change: spy,
+ });
+
+ expect(component.$el.querySelector('.js-next-button').textContent.trim()).toEqual('Next');
+
+ component.$el.querySelector('.js-next-button .page-link').click();
expect(spy).not.toHaveBeenCalled();
});
@@ -159,7 +257,25 @@ describe('Pagination component', () => {
change: spy,
});
- component.$el.querySelector('.js-next-button a').click();
+ component.$el.querySelector('.js-next-button .page-link').click();
+
+ expect(spy).toHaveBeenCalledWith(4);
+ });
+
+ it('should be enabled and clickable when total and totalPages are NaN', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: 4,
+ page: 3,
+ perPage: 20,
+ previousPage: 2,
+ total: NaN,
+ totalPages: NaN,
+ },
+ change: spy,
+ });
+
+ component.$el.querySelector('.js-next-button .page-link').click();
expect(spy).toHaveBeenCalledWith(4);
});
@@ -181,22 +297,56 @@ describe('Pagination component', () => {
expect(component.$el.querySelectorAll('.page').length).toEqual(5);
});
+
+ it('should not render any page', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: 4,
+ page: 3,
+ perPage: 20,
+ previousPage: 2,
+ total: NaN,
+ totalPages: NaN,
+ },
+ change: spy,
+ });
+
+ expect(component.$el.querySelectorAll('.page').length).toEqual(0);
+ });
});
- it('should render the spread operator', () => {
- component = mountComponent({
- pageInfo: {
- nextPage: 4,
- page: 3,
- perPage: 20,
- previousPage: 2,
- total: 84,
- totalPages: 10,
- },
- change: spy,
+ describe('spread operator', () => {
+ it('should render', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: 4,
+ page: 3,
+ perPage: 20,
+ previousPage: 2,
+ total: 84,
+ totalPages: 10,
+ },
+ change: spy,
+ });
+
+ expect(component.$el.querySelector('.separator').textContent.trim()).toEqual('...');
});
- expect(component.$el.querySelector('.separator').textContent.trim()).toEqual('...');
+ it('should not render', () => {
+ component = mountComponent({
+ pageInfo: {
+ nextPage: 4,
+ page: 3,
+ perPage: 20,
+ previousPage: 2,
+ total: NaN,
+ totalPages: NaN,
+ },
+ change: spy,
+ });
+
+ expect(component.$el.querySelector('.separator')).toBeNull();
+ });
});
});
});
diff --git a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
index e8b41e8eeff..c7e0d806d80 100644
--- a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
@@ -17,7 +17,7 @@ const DEFAULT_PROPS = {
const UserPopover = Vue.extend(userPopover);
describe('User Popover Component', () => {
- const fixtureTemplate = 'merge_requests/diff_comment.html.raw';
+ const fixtureTemplate = 'merge_requests/diff_comment.html';
preloadFixtures(fixtureTemplate);
let vm;
@@ -61,6 +61,12 @@ describe('User Popover Component', () => {
expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.username);
expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.location);
});
+
+ it('shows icon for location', () => {
+ const iconEl = vm.$el.querySelector('.js-location svg');
+
+ expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('location');
+ });
});
describe('job data', () => {
@@ -117,6 +123,18 @@ describe('User Popover Component', () => {
'Me & my <funky> Company',
);
});
+
+ it('shows icon for bio', () => {
+ const iconEl = vm.$el.querySelector('.js-bio svg');
+
+ expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('profile');
+ });
+
+ it('shows icon for organization', () => {
+ const iconEl = vm.$el.querySelector('.js-organization svg');
+
+ expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('work');
+ });
});
describe('status data', () => {
diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js
index adb5ff682f0..0aaa4050cba 100644
--- a/spec/javascripts/vue_shared/translate_spec.js
+++ b/spec/javascripts/vue_shared/translate_spec.js
@@ -3,7 +3,7 @@ import Jed from 'jed';
import locale from '~/locale';
import Translate from '~/vue_shared/translate';
-import { trimText } from 'spec/helpers/vue_component_helper';
+import { trimText } from 'spec/helpers/text_helper';
describe('Vue translate filter', () => {
let el;
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index e5f1e6ae937..8f662c71c7a 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -6,7 +6,7 @@ import ZenMode from '~/zen_mode';
describe('ZenMode', () => {
let zen;
let dropzoneForElementSpy;
- const fixtureName = 'snippets/show.html.raw';
+ const fixtureName = 'snippets/show.html';
preloadFixtures(fixtureName);
diff --git a/spec/lib/api/entities/job_request/image_spec.rb b/spec/lib/api/entities/job_request/image_spec.rb
new file mode 100644
index 00000000000..092c181ae9c
--- /dev/null
+++ b/spec/lib/api/entities/job_request/image_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Entities::JobRequest::Image do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'name' }]}
+ let(:image) { double(name: 'image_name', entrypoint: ['foo'], ports: ports)}
+ let(:entity) { described_class.new(image) }
+
+ subject { entity.as_json }
+
+ it 'returns the image name' do
+ expect(subject[:name]).to eq 'image_name'
+ end
+
+ it 'returns the entrypoint' do
+ expect(subject[:entrypoint]).to eq ['foo']
+ end
+
+ it 'returns the ports' do
+ expect(subject[:ports]).to eq ports
+ end
+
+ context 'when the ports param is nil' do
+ let(:ports) { nil }
+
+ it 'does not return the ports' do
+ expect(subject[:ports]).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/api/entities/job_request/port_spec.rb b/spec/lib/api/entities/job_request/port_spec.rb
new file mode 100644
index 00000000000..40ab4cd6231
--- /dev/null
+++ b/spec/lib/api/entities/job_request/port_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::API::Entities::JobRequest::Port do
+ let(:port) { double(number: 80, protocol: 'http', name: 'name')}
+ let(:entity) { described_class.new(port) }
+
+ subject { entity.as_json }
+
+ it 'returns the port number' do
+ expect(subject[:number]).to eq 80
+ end
+
+ it 'returns if the port protocol' do
+ expect(subject[:protocol]).to eq 'http'
+ end
+
+ it 'returns the port name' do
+ expect(subject[:name]).to eq 'name'
+ end
+end
diff --git a/spec/lib/api/helpers/custom_validators_spec.rb b/spec/lib/api/helpers/custom_validators_spec.rb
index 41e6fb47b11..aed86b21cb7 100644
--- a/spec/lib/api/helpers/custom_validators_spec.rb
+++ b/spec/lib/api/helpers/custom_validators_spec.rb
@@ -21,7 +21,7 @@ describe API::Helpers::CustomValidators do
end
context 'invalid parameters' do
- it 'should raise a validation error' do
+ it 'raises a validation error' do
expect_validation_error({ 'test' => 'some_value' })
end
end
@@ -44,7 +44,30 @@ describe API::Helpers::CustomValidators do
end
context 'invalid parameters' do
- it 'should raise a validation error' do
+ it 'raises a validation error' do
+ expect_validation_error({ 'test' => 'some_other_string' })
+ end
+ end
+ end
+
+ describe API::Helpers::CustomValidators::ArrayNoneAny do
+ subject do
+ described_class.new(['test'], {}, false, scope.new)
+ end
+
+ context 'valid parameters' do
+ it 'does not raise a validation error' do
+ expect_no_validation_error({ 'test' => [] })
+ expect_no_validation_error({ 'test' => [1, 2, 3] })
+ expect_no_validation_error({ 'test' => 'None' })
+ expect_no_validation_error({ 'test' => 'Any' })
+ expect_no_validation_error({ 'test' => 'none' })
+ expect_no_validation_error({ 'test' => 'any' })
+ end
+ end
+
+ context 'invalid parameters' do
+ it 'raises a validation error' do
expect_validation_error({ 'test' => 'some_other_string' })
end
end
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index 6e215ea1561..c788da55cd2 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -2,8 +2,12 @@ require 'spec_helper'
describe API::Helpers::Pagination do
let(:resource) { Project.all }
- let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:8080/api/v4/projects" }
- let(:canonical_api_projects_url) { "#{Gitlab.config.gitlab.url}/api/v4/projects" }
+ let(:custom_port) { 8080 }
+ let(:incoming_api_projects_url) { "#{Gitlab.config.gitlab.url}:#{custom_port}/api/v4/projects" }
+
+ before do
+ stub_config_setting(port: custom_port)
+ end
subject do
Class.new.include(described_class).new
@@ -48,7 +52,7 @@ describe API::Helpers::Pagination do
it 'adds appropriate headers' do
expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}")
+ expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[1].id).to_query}")
expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"')
@@ -71,7 +75,7 @@ describe API::Helpers::Pagination do
it 'adds appropriate headers' do
expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}")
+ expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[2].id).to_query}")
expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"')
@@ -171,7 +175,7 @@ describe API::Helpers::Pagination do
it 'returns the right link to the next page' do
expect_header('X-Per-Page', '2')
- expect_header('X-Next-Page', "#{canonical_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}")
+ expect_header('X-Next-Page', "#{incoming_api_projects_url}?#{query.merge(ks_prev_id: projects[6].id, ks_prev_name: projects[6].name).to_query}")
expect_header('Link', anything) do |_key, val|
expect(val).to include('rel="next"')
end
@@ -224,9 +228,9 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="prev"')
end
@@ -290,8 +294,8 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="next"))
expect(val).not_to include('rel="last"')
expect(val).not_to include('rel="prev"')
end
@@ -318,9 +322,9 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '1')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 2).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="prev"))
expect(val).not_to include('rel="next"')
end
@@ -367,8 +371,8 @@ describe API::Helpers::Pagination do
expect_header('X-Prev-Page', '')
expect_header('Link', anything) do |_key, val|
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
- expect(val).to include(%Q(<#{canonical_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="first"))
+ expect(val).to include(%Q(<#{incoming_api_projects_url}?#{query.merge(page: 1).to_query}>; rel="last"))
expect(val).not_to include('rel="prev"')
expect(val).not_to include('rel="next"')
expect(val).not_to include('page=0')
diff --git a/spec/lib/api/helpers/related_resources_helpers_spec.rb b/spec/lib/api/helpers/related_resources_helpers_spec.rb
index 66af7f81535..99fe8795d91 100644
--- a/spec/lib/api/helpers/related_resources_helpers_spec.rb
+++ b/spec/lib/api/helpers/related_resources_helpers_spec.rb
@@ -5,6 +5,40 @@ describe API::Helpers::RelatedResourcesHelpers do
Class.new.include(described_class).new
end
+ describe '#expose_path' do
+ let(:path) { '/api/v4/awesome_endpoint' }
+
+ context 'empty relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq(path)
+ end
+ end
+
+ context 'slash relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '/')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq(path)
+ end
+ end
+
+ context 'with relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq("/gitlab/root" + path)
+ end
+ end
+ end
+
describe '#expose_url' do
let(:path) { '/api/v4/awesome_endpoint' }
subject(:url) { helpers.expose_url(path) }
diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb
index 08165f147bb..00916f80784 100644
--- a/spec/lib/api/helpers_spec.rb
+++ b/spec/lib/api/helpers_spec.rb
@@ -137,18 +137,6 @@ describe API::Helpers do
it_behaves_like 'user namespace finder'
end
- describe '#user_namespace' do
- let(:namespace_finder) do
- subject.user_namespace
- end
-
- before do
- allow(subject).to receive(:params).and_return({ id: namespace.id })
- end
-
- it_behaves_like 'user namespace finder'
- end
-
describe '#send_git_blob' do
let(:repository) { double }
let(:blob) { double(name: 'foobar') }
diff --git a/spec/lib/backup/uploads_spec.rb b/spec/lib/backup/uploads_spec.rb
new file mode 100644
index 00000000000..544d3754c0f
--- /dev/null
+++ b/spec/lib/backup/uploads_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe Backup::Uploads do
+ let(:progress) { StringIO.new }
+ subject(:backup) { described_class.new(progress) }
+
+ describe '#initialize' do
+ it 'uses the correct upload dir' do
+ Dir.mktmpdir do |tmpdir|
+ FileUtils.mkdir_p("#{tmpdir}/uploads")
+
+ allow(Gitlab.config.uploads).to receive(:storage_path) { tmpdir }
+
+ expect(backup.app_files_dir).to eq("#{tmpdir}/uploads")
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb
index 1f53657c59c..316dbf052c3 100644
--- a/spec/lib/banzai/commit_renderer_spec.rb
+++ b/spec/lib/banzai/commit_renderer_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Banzai::CommitRenderer do
- describe '.render' do
+ describe '.render', :clean_gitlab_redis_cache do
it 'renders a commit description and title' do
user = build(:user)
project = create(:project, :repository)
@@ -13,7 +13,7 @@ describe Banzai::CommitRenderer do
described_class::ATTRIBUTES.each do |attr|
expect_any_instance_of(Banzai::ObjectRenderer).to receive(:render).with([project.commit], attr).once.and_call_original
- expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, {})
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(project.commit, attr, { skip_project_check: false }).and_call_original
end
described_class.render([project.commit], project, user)
diff --git a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
index b645e49bd43..5b3f679084e 100644
--- a/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
+++ b/spec/lib/banzai/filter/blockquote_fence_filter_spec.rb
@@ -13,6 +13,6 @@ describe Banzai::Filter::BlockquoteFenceFilter do
end
it 'allows trailing whitespace on blockquote fence lines' do
- expect(filter(">>> \ntest\n>>> ")).to eq("> test")
+ expect(filter(">>> \ntest\n>>> ")).to eq("\n> test\n")
end
end
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index a0270d93d50..7c94cf37e32 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -121,6 +121,49 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do
end
end
+ context "youtrack project" do
+ let(:project) { create(:youtrack_project) }
+
+ before do
+ project.update!(issues_enabled: false)
+ end
+
+ context "with right markdown" do
+ let(:issue) { ExternalIssue.new("YT-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with underscores in the prefix" do
+ let(:issue) { ExternalIssue.new("PRJ_1-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with lowercase letters in the prefix" do
+ let(:issue) { ExternalIssue.new("YTkPrj-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with a single-letter prefix" do
+ let(:issue) { ExternalIssue.new("T-123", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+
+ context "with a lowercase prefix" do
+ let(:issue) { ExternalIssue.new("gl-030", project) }
+ let(:reference) { issue.to_reference }
+
+ it_behaves_like "external issue tracker"
+ end
+ end
+
context "jira project" do
let(:project) { create(:jira_project) }
let(:reference) { issue.to_reference }
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 55c41e55437..72dfd6ff9ea 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -30,6 +30,23 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
end
end
+ describe 'all references' do
+ let(:doc) { reference_filter(merge.to_reference) }
+ let(:tag_el) { doc.css('a').first }
+
+ it 'adds merge request iid' do
+ expect(tag_el["data-iid"]).to eq(merge.iid.to_s)
+ end
+
+ it 'adds project data attribute with project id' do
+ expect(tag_el["data-project-path"]).to eq(project.full_path)
+ end
+
+ it 'does not add `has-tooltip` class' do
+ expect(tag_el["class"]).not_to include('has-tooltip')
+ end
+ end
+
context 'internal reference' do
let(:reference) { merge.to_reference }
@@ -57,9 +74,9 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
expect(reference_filter(act).to_html).to eq exp
end
- it 'includes a title attribute' do
+ it 'has no title' do
doc = reference_filter("Merge #{reference}")
- expect(doc.css('a').first.attr('title')).to eq merge.title
+ expect(doc.css('a').first.attr('title')).to eq ""
end
it 'escapes the title attribute' do
@@ -69,9 +86,9 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
expect(doc.text).to eq "Merge #{reference}"
end
- it 'includes default classes' do
+ it 'includes default classes, without tooltip' do
doc = reference_filter("Merge #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request has-tooltip'
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
end
it 'includes a data-project attribute' do
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
index 4c94e4fdae0..f0a5dc8d0d7 100644
--- a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -295,6 +295,25 @@ describe Banzai::Filter::MilestoneReferenceFilter do
end
end
+ shared_examples 'references with HTML entities' do
+ before do
+ milestone.update!(title: '&lt;html&gt;')
+ end
+
+ it 'links to a valid reference' do
+ doc = reference_filter('See %"&lt;html&gt;"')
+
+ expect(doc.css('a').first.attr('href')).to eq urls.milestone_url(milestone)
+ expect(doc.text).to eq 'See %<html>'
+ end
+
+ it 'ignores invalid milestone names and escapes entities' do
+ act = %(Milestone %"&lt;non valid&gt;")
+
+ expect(reference_filter(act).to_html).to eq act
+ end
+ end
+
shared_context 'project milestones' do
let(:reference) { milestone.to_reference(format: :iid) }
@@ -307,6 +326,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
it_behaves_like 'cross-project / cross-namespace complete reference'
it_behaves_like 'cross-project / same-namespace complete reference'
it_behaves_like 'cross project shorthand reference'
+ it_behaves_like 'references with HTML entities'
end
shared_context 'group milestones' do
@@ -317,6 +337,7 @@ describe Banzai::Filter::MilestoneReferenceFilter do
it_behaves_like 'String-based single-word references'
it_behaves_like 'String-based multi-word references in quotes'
it_behaves_like 'referencing a milestone in a link href'
+ it_behaves_like 'references with HTML entities'
it 'does not support references by IID' do
doc = reference_filter("See #{Milestone.reference_prefix}#{milestone.iid}")
diff --git a/spec/lib/banzai/filter/output_safety_spec.rb b/spec/lib/banzai/filter/output_safety_spec.rb
new file mode 100644
index 00000000000..5ffe591c9a4
--- /dev/null
+++ b/spec/lib/banzai/filter/output_safety_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::OutputSafety do
+ subject do
+ Class.new do
+ include Banzai::Filter::OutputSafety
+ end.new
+ end
+
+ let(:content) { '<pre><code>foo</code></pre>' }
+
+ context 'when given HTML is safe' do
+ let(:html) { content.html_safe }
+
+ it 'returns safe HTML' do
+ expect(subject.escape_once(html)).to eq(html)
+ end
+ end
+
+ context 'when given HTML is not safe' do
+ let(:html) { content }
+
+ it 'returns escaped HTML' do
+ expect(subject.escape_once(html)).to eq(ERB::Util.html_escape_once(html))
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/plantuml_filter_spec.rb b/spec/lib/banzai/filter/plantuml_filter_spec.rb
index 8235c411eb7..6f7acfe7072 100644
--- a/spec/lib/banzai/filter/plantuml_filter_spec.rb
+++ b/spec/lib/banzai/filter/plantuml_filter_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Banzai::Filter::PlantumlFilter do
include FilterSpecHelper
- it 'should replace plantuml pre tag with img tag' do
+ 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>'
@@ -12,7 +12,7 @@ describe Banzai::Filter::PlantumlFilter do
expect(doc.to_s).to eq output
end
- it 'should not replace plantuml pre tag with img tag if disabled' do
+ 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>'
@@ -21,7 +21,7 @@ describe Banzai::Filter::PlantumlFilter do
expect(doc.to_s).to eq output
end
- it 'should not replace plantuml pre tag with img tag if url is invalid' do
+ 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"> PlantUML Error: cannot connect to PlantUML server at "invalid"</pre></div></div>'
diff --git a/spec/lib/banzai/filter/suggestion_filter_spec.rb b/spec/lib/banzai/filter/suggestion_filter_spec.rb
index b13c90b54bd..9c4650b73de 100644
--- a/spec/lib/banzai/filter/suggestion_filter_spec.rb
+++ b/spec/lib/banzai/filter/suggestion_filter_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe Banzai::Filter::SuggestionFilter do
include FilterSpecHelper
- let(:input) { "<pre class='code highlight js-syntax-highlight suggestion'><code>foo\n</code></pre>" }
+ let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion"><code>foo\n</code></pre>) }
let(:default_context) do
{ suggestions_filter_enabled: true }
end
@@ -23,4 +23,16 @@ describe Banzai::Filter::SuggestionFilter do
expect(result[:class]).to be_nil
end
+
+ context 'multi-line suggestions' do
+ let(:data_attr) { Banzai::Filter::SyntaxHighlightFilter::LANG_PARAMS_ATTR }
+ let(:input) { %(<pre class="code highlight js-syntax-highlight suggestion" #{data_attr}="-3+2"><code>foo\n</code></pre>) }
+
+ it 'element has correct data-lang-params' do
+ doc = filter(input, default_context)
+ pre = doc.css('pre').first
+
+ expect(pre[data_attr]).to eq('-3+2')
+ 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 ef52c572898..80ca7a63435 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -45,7 +45,10 @@ describe Banzai::Filter::SyntaxHighlightFilter do
end
context "languages that should be passed through" do
- %w(math mermaid plantuml).each do |lang|
+ 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 = filter(%{<pre><code lang="#{lang}">This is a test</code></pre>})
@@ -55,6 +58,33 @@ describe Banzai::Filter::SyntaxHighlightFilter do
include_examples "XSS prevention", lang
end
+
+ context "when #{lang} has extra params" do
+ let(:lang_params) { 'foo-bar-kux' }
+
+ 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>})
+
+ expect(result.to_html).to eq(%{<pre class="code highlight js-syntax-highlight #{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
+ 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>"
+ end
+ end
+
+ context 'when multiple param delimiters are used' do
+ let(:lang) { 'suggestion' }
+ let(:lang_params) { '-1+10' }
+
+ 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 #{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
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
index 7213cd58ea7..4a9880ac85a 100644
--- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -58,6 +58,11 @@ describe Banzai::Filter::TableOfContentsFilter do
expect(doc.css('h1 a').first.attr('href')).to eq '#this-header-is-filled-with-punctuation'
end
+ it 'removes any leading or trailing spaces' do
+ doc = filter(header(1, " \r\n\tTitle with spaces\r\n\t "))
+ expect(doc.css('h1 a').first.attr('href')).to eq '#title-with-spaces'
+ end
+
it 'appends a unique number to duplicates' do
doc = filter(header(1, 'One') + header(2, 'One'))
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
index b9059b85fdc..cce1cd0b284 100644
--- a/spec/lib/banzai/filter/wiki_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -70,5 +70,47 @@ describe Banzai::Filter::WikiLinkFilter do
expect(filtered_link.attribute('href').value).to eq(invalid_link)
end
end
+
+ context "when the slug is deemed unsafe or invalid" do
+ let(:link) { "alert(1);" }
+
+ invalid_slugs = [
+ "javascript:",
+ "JaVaScRiPt:",
+ "\u0001java\u0003script:",
+ "javascript :",
+ "javascript: ",
+ "javascript : ",
+ ":javascript:",
+ "javascript&#58;",
+ "javascript&#0058;",
+ "javascript&#x3A;",
+ "javascript&#x003A;",
+ "java\0script:",
+ " &#14; javascript:"
+ ]
+
+ invalid_slugs.each do |slug|
+ context "with the slug #{slug}" do
+ it "doesn't rewrite a (.) relative link" do
+ filtered_link = filter(
+ "<a href='.#{link}'>Link</a>",
+ project_wiki: wiki,
+ page_slug: slug).children[0]
+
+ expect(filtered_link.attribute('href').value).not_to include(slug)
+ end
+
+ it "doesn't rewrite a (..) relative link" do
+ filtered_link = filter(
+ "<a href='..#{link}'>Link</a>",
+ project_wiki: wiki,
+ page_slug: slug).children[0]
+
+ expect(filtered_link.attribute('href').value).not_to include(slug)
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index 3b52f6666d0..7b855251a74 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -11,7 +11,7 @@ describe Banzai::ObjectRenderer do
)
end
- let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16) }
+ let(:object) { Note.new(note: 'hello', note_html: '<p dir="auto">hello</p>', cached_markdown_version: Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16) }
describe '#render' do
context 'with cache' do
@@ -60,24 +60,38 @@ describe Banzai::ObjectRenderer do
end
context 'without cache' do
- let(:commit) { project.commit }
+ let(:cacheless_class) do
+ Class.new do
+ attr_accessor :title, :redacted_title_html, :project
+
+ def banzai_render_context(field)
+ { project: project, pipeline: :single_line }
+ end
+ end
+ end
+ let(:cacheless_thing) do
+ cacheless_class.new.tap do |thing|
+ thing.title = "Merge branch 'branch-merged' into 'master'"
+ thing.project = project
+ end
+ end
it 'renders and redacts an Array of objects' do
- renderer.render([commit], :title)
+ renderer.render([cacheless_thing], :title)
- expect(commit.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'")
+ expect(cacheless_thing.redacted_title_html).to eq("Merge branch 'branch-merged' into 'master'")
end
it 'calls Banzai::Redactor to perform redaction' do
expect_any_instance_of(Banzai::Redactor).to receive(:redact).and_call_original
- renderer.render([commit], :title)
+ renderer.render([cacheless_thing], :title)
end
it 'retrieves field content using Banzai::Renderer.cacheless_render_field' do
- expect(Banzai::Renderer).to receive(:cacheless_render_field).with(commit, :title, {}).and_call_original
+ expect(Banzai::Renderer).to receive(:cacheless_render_field).with(cacheless_thing, :title, {}).and_call_original
- renderer.render([commit], :title)
+ renderer.render([cacheless_thing], :title)
end
end
end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index aaeec953e4b..718649e0e10 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -13,10 +13,10 @@ describe Banzai::Redactor do
it 'redacts an array of documents' do
doc1 = Nokogiri::HTML
- .fragment('<a class="gfm" data-reference-type="issue">foo</a>')
+ .fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">foo</a>')
doc2 = Nokogiri::HTML
- .fragment('<a class="gfm" data-reference-type="issue">bar</a>')
+ .fragment('<a class="gfm" href="https://www.gitlab.com" data-reference-type="issue">bar</a>')
redacted_data = redactor.redact([doc1, doc2])
@@ -27,7 +27,7 @@ describe Banzai::Redactor do
end
it 'replaces redacted reference with inner HTML' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue'>foo</a>")
redactor.redact([doc])
expect(doc.to_html).to eq('foo')
end
@@ -35,20 +35,24 @@ describe Banzai::Redactor do
context 'when data-original attribute provided' do
let(:original_content) { '<code>foo</code>' }
it 'replaces redacted reference with original content' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
redactor.redact([doc])
expect(doc.to_html).to eq(original_content)
end
- end
-
- it 'returns <a> tag with original href if it is originally a link reference' do
- href = 'http://localhost:3000'
- doc = Nokogiri::HTML
- .fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>")
- redactor.redact([doc])
+ it 'does not replace redacted reference with original content if href is given' do
+ html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Marge'>Marge</a>"
+ doc = Nokogiri::HTML.fragment(html)
+ redactor.redact([doc])
+ expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Marge</a>')
+ end
- expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>')
+ it 'uses the original content as the link content if given' do
+ html = "<a href='https://www.gitlab.com' data-link-reference='true' class='gfm' data-reference-type='issue' data-reference-type='issue' data-original='Homer'>Marge</a>"
+ doc = Nokogiri::HTML.fragment(html)
+ redactor.redact([doc])
+ expect(doc.to_html).to eq('<a href="https://www.gitlab.com">Homer</a>')
+ end
end
end
@@ -61,7 +65,7 @@ describe Banzai::Redactor do
end
it 'redacts an issue attached' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-issue='#{issue.id}'>foo</a>")
redactor.redact([doc])
@@ -69,7 +73,7 @@ describe Banzai::Redactor do
end
it 'redacts an external issue' do
- doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
+ doc = Nokogiri::HTML.fragment("<a class='gfm' href='https://www.gitlab.com' data-reference-type='issue' data-external-issue='#{issue.id}' data-project='#{project.id}'>foo</a>")
redactor.redact([doc])
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
index 650cecfc778..aa828e2f0e9 100644
--- a/spec/lib/banzai/renderer_spec.rb
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -11,16 +11,24 @@ describe Banzai::Renderer do
object
end
+ def fake_cacheless_object
+ object = double('cacheless object')
+
+ allow(object).to receive(:respond_to?).with(:cached_markdown_fields).and_return(false)
+
+ object
+ end
+
describe '#render_field' do
let(:renderer) { described_class }
context 'without cache' do
- let(:commit) { create(:project, :repository).commit }
+ let(:commit) { fake_cacheless_object }
it 'returns cacheless render field' do
- expect(renderer).to receive(:cacheless_render_field).with(commit, :title, {})
+ expect(renderer).to receive(:cacheless_render_field).with(commit, :field, {})
- renderer.render_field(commit, :title)
+ renderer.render_field(commit, :field)
end
end
diff --git a/spec/lib/banzai/suggestions_parser_spec.rb b/spec/lib/banzai/suggestions_parser_spec.rb
deleted file mode 100644
index 79658d710ce..00000000000
--- a/spec/lib/banzai/suggestions_parser_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Banzai::SuggestionsParser do
- describe '.parse' do
- it 'returns a list of suggestion contents' do
- markdown = <<-MARKDOWN.strip_heredoc
- ```suggestion
- foo
- bar
- ```
-
- ```
- nothing
- ```
-
- ```suggestion
- xpto
- baz
- ```
-
- ```thing
- this is not a suggestion, it's a thing
- ```
- MARKDOWN
-
- expect(described_class.parse(markdown)).to eq([" foo\n bar",
- " xpto\n baz"])
- end
- end
-end
diff --git a/spec/lib/constraints/project_url_constrainer_spec.rb b/spec/lib/constraints/project_url_constrainer_spec.rb
index c96e7ab8495..3496b01ebcc 100644
--- a/spec/lib/constraints/project_url_constrainer_spec.rb
+++ b/spec/lib/constraints/project_url_constrainer_spec.rb
@@ -16,6 +16,10 @@ describe Constraints::ProjectUrlConstrainer do
let(:request) { build_request('foo', 'bar') }
it { expect(subject.matches?(request)).to be_falsey }
+
+ context 'existence_check is false' do
+ it { expect(subject.matches?(request, existence_check: false)).to be_truthy }
+ end
end
context "project id ending with .git" do
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
index 30016da6828..6648e141b7a 100644
--- a/spec/lib/event_filter_spec.rb
+++ b/spec/lib/event_filter_spec.rb
@@ -26,10 +26,10 @@ describe EventFilter do
set(:push_event) { create(:push_event, project: public_project) }
set(:merged_event) { create(:event, :merged, project: public_project, target: public_project) }
- set(:created_event) { create(:event, :created, project: public_project, target: public_project) }
- set(:updated_event) { create(:event, :updated, project: public_project, target: public_project) }
- set(:closed_event) { create(:event, :closed, project: public_project, target: public_project) }
- set(:reopened_event) { create(:event, :reopened, project: public_project, target: public_project) }
+ set(:created_event) { create(:event, :created, project: public_project, target: create(:issue, project: public_project)) }
+ set(:updated_event) { create(:event, :updated, project: public_project, target: create(:issue, project: public_project)) }
+ set(:closed_event) { create(:event, :closed, project: public_project, target: create(:issue, project: public_project)) }
+ set(:reopened_event) { create(:event, :reopened, project: public_project, target: create(:issue, project: public_project)) }
set(:comments_event) { create(:event, :commented, project: public_project, target: public_project) }
set(:joined_event) { create(:event, :joined, project: public_project, target: public_project) }
set(:left_event) { create(:event, :left, project: public_project, target: public_project) }
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index e0691aba600..21ba72953fb 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -44,6 +44,36 @@ describe ExtractsPath do
end
end
+ context 'ref contains trailing space' do
+ let(:ref) { 'master ' }
+
+ it 'strips surrounding space' do
+ assign_ref_vars
+
+ expect(@ref).to eq('master')
+ end
+ end
+
+ context 'ref contains leading space' do
+ let(:ref) { ' master ' }
+
+ it 'strips surrounding space' do
+ assign_ref_vars
+
+ expect(@ref).to eq('master')
+ end
+ end
+
+ context 'ref contains space in the middle' do
+ let(:ref) { 'master plan ' }
+
+ it 'returns 404' do
+ expect(self).to receive(:render_404)
+
+ assign_ref_vars
+ end
+ end
+
context 'path contains space' do
let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb
index 494c0561975..b9ffe895bf0 100644
--- a/spec/lib/forever_spec.rb
+++ b/spec/lib/forever_spec.rb
@@ -5,7 +5,7 @@ describe Forever do
subject { described_class.date }
context 'when using PostgreSQL' do
- it 'should return Postgresql future date' do
+ it 'returns Postgresql future date' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
expect(subject).to eq(described_class::POSTGRESQL_DATE)
@@ -13,7 +13,7 @@ describe Forever do
end
context 'when using MySQL' do
- it 'should return MySQL future date' do
+ it 'returns MySQL future date' do
allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
expect(subject).to eq(described_class::MYSQL_DATE)
diff --git a/spec/lib/gitlab/auth/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index d3ab599d5a0..b91a09e3137 100644
--- a/spec/lib/gitlab/auth/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -5,6 +5,65 @@ describe Gitlab::Auth::LDAP::Config do
let(:config) { described_class.new('ldapmain') }
+ def raw_cert
+ <<-EOS
+-----BEGIN CERTIFICATE-----
+MIIDZjCCAk4CCQDX+u/9fICksDANBgkqhkiG9w0BAQsFADB1MQswCQYDVQQGEwJV
+UzEMMAoGA1UECAwDRm9vMQwwCgYDVQQHDANCYXIxDDAKBgNVBAoMA0JhejEMMAoG
+A1UECwwDUXV4MQ0wCwYDVQQDDARsZGFwMR8wHQYJKoZIhvcNAQkBFhBsZGFwQGV4
+YW1wbGUuY29tMB4XDTE5MDIyNzE1NTUxNFoXDTE5MDMyOTE1NTUxNFowdTELMAkG
+A1UEBhMCVVMxDDAKBgNVBAgMA0ZvbzEMMAoGA1UEBwwDQmFyMQwwCgYDVQQKDANC
+YXoxDDAKBgNVBAsMA1F1eDENMAsGA1UEAwwEbGRhcDEfMB0GCSqGSIb3DQEJARYQ
+bGRhcEBleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+APuDB/4/AUmTEmhYzN13no4Kt8hkRbLQuENRHlOeQw05/MVdoB1AWLOPzIXn4kex
+GD9tHkoJl8S0QPmAAcPHn5O97e+gd0ze5dRQZl/cSd2/j5zeaMvZ1mCrPN/dOluM
+94Oj+wQU4bEcOlrqIMSh0ezJw10R3IHXCQFeGtIZU57WmKcrryQX4kP7KTOgRw/t
+CYp+NivQHtLbBEj1MU0l10qMS2+w8Qpqov4MdW4gx4wTgId2j1ZZ56+n6Jsc9qoI
+wBWBNL4XU5a3kwhYZDOJoOvI9po33KLdT1dXS81uOFXClp3LGmKDgLTwQ1w+RmQG
++JG4EvTfDIShdcTDXEaOfCECAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAJM9Btu5g
+k8qDiz5TilvpyoGuI4viCwusARFAFmOB/my/cHlVvkuq4bbfV1KJoWWGJg8GcklL
+cnIdxc35uYM5icr6xXQyrW0GqAO+LEXyUxVQqYETxrQ/LJ03xhBnuF7hvZJIBiky
+GwUy0clJxGfaCeEM8zXwePawLgGjuUawDDQOwigysoWqoMu3VFW8zl8UPa84bow9
+Kn2QmPAkLw4EcqYSCNSSvnyzu5SM64jwLWRXFsmlqD7773oT29vTkqM1EQANFEfT
+7gQomLyPqoPBoFph5oSNn6Rf31QX1Sie92EAKVnZ1XmD68hKzjv6ChCtzTv4jABg
+XrDwnLkORIAF/Q==
+-----END CERTIFICATE-----
+ EOS
+ end
+
+ def raw_key
+ <<-EOS
+-----BEGIN PRIVATE KEY-----
+MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQD7gwf+PwFJkxJo
+WMzdd56OCrfIZEWy0LhDUR5TnkMNOfzFXaAdQFizj8yF5+JHsRg/bR5KCZfEtED5
+gAHDx5+Tve3voHdM3uXUUGZf3Endv4+c3mjL2dZgqzzf3TpbjPeDo/sEFOGxHDpa
+6iDEodHsycNdEdyB1wkBXhrSGVOe1pinK68kF+JD+ykzoEcP7QmKfjYr0B7S2wRI
+9TFNJddKjEtvsPEKaqL+DHVuIMeME4CHdo9WWeevp+ibHPaqCMAVgTS+F1OWt5MI
+WGQziaDryPaaN9yi3U9XV0vNbjhVwpadyxpig4C08ENcPkZkBviRuBL03wyEoXXE
+w1xGjnwhAgMBAAECggEAbw82GVui6uUpjLAhjm3CssAi1TcJ2+L0aq1IMe5Bd3ay
+mkg0apY+VNPboQl6zuNxbJh3doPz42UhB8sxfE0Ktwd4KIb4Bxap7+2stwmkCGoN
+NVy0c8d2NWuHzuZ2XXTK2vMu5Wd/HWD0l66o14sJEoEpZlB7yU216UevmjSayxjh
+aBTSaYyyrf24haTaCuqwph/V73ZlMpFdSALGny0uiP/5inxciMCkMpHfX6BflSb4
+EGKsIYt9BJ0kY4GNG5bCP7971UCxp2eEJhU2fV8HuFGCOD12IqSpUqPxHxjsWpfx
+T7FZ3V2kM/58Ca+5LB2y3atcPIdY0/g7/43V4VD+7QKBgQD/PO4/0cmZuuLU1LPT
+C/C596kPK0JLlvvRqhbz4byRAkW/n7uQFG7TMtFNle3UmT7rk7pjtbHnByqzEd+9
+jMhBysjHOMg0+DWm7fEtSg/tJ3qLVO3nbdA4qmXYobLcLoG+PCYRLskEHHqTG/Bv
+QZLbavOU6rrTqckNr1TMpNBmXwKBgQD8Q0C2YTOpwgjRUe8i6Chnc3o4x8a1i98y
+9la6c7y7acWHSbEczMkNfEBrbM73rTb+bBA0Zqw+Z1gkv8bGpvGxX8kbSfJJ2YKW
+9koxpLNTVNVapqBa9ImiaozV285dz9Ukx8bnMOJlTELpOl7RRV7iF0smYjfHIl3D
+Yxyda/MtfwKBgHb9l/Dmw77IkqE4PFFimqqIHCe3OiP1UpavXh36midcUNoCBLYp
+4HTTlyI9iG/5tYysBVQgy7xx6eUrqww6Ss3pVOsTvLp9EL4u5aYAhiZApm+4e2TO
+HCmevvZcg/8EK3Zdoj2Wex5QjJBykQe9IVLrrH07ZTfySon3uGfjWkivAoGAGvqS
+VC8HGHOw/7n0ilYr5Ax8mM/813OzFj80PVKdb6m7P2HJOFxKcE/Gj/aeF+0FgaZL
+AV+tsirZSWzdNGesV5z35Bw/dlh11/FVNAP6TcI34y8I3VFj2uPsVf7hDjVpBTr8
+ccNPoyfJzCm69ESoBiQZnGxKrNhnELtr1wYxhr8CgYApWwf4hVrTWV1zs+pEJenh
+AtlErSqafbECNDSwS5BX8yDpu5yRBJ4xegO/rNlmb8ICRYkuJapD1xXicFOsmfUK
+0Ff8afd2Q/OfBeUdq9KA4JO9fNqzEwOWvv8Ryn4ZSYcAuLP7IVJKjjI6R7rYaO/G
+3OWJdizbykGOi0BFDu+3dw==
+-----END PRIVATE KEY-----
+ EOS
+ end
+
describe '.servers' do
it 'returns empty array if no server information is available' do
allow(Gitlab.config).to receive(:ldap).and_return('enabled' => false)
@@ -89,6 +148,42 @@ describe Gitlab::Auth::LDAP::Config do
expect(config.adapter_options[:encryption]).to include({ method: :start_tls })
end
+ it 'transforms SSL cert and key to OpenSSL objects' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'start_tls',
+ 'tls_options' => {
+ 'cert' => raw_cert,
+ 'key' => raw_key
+ }
+ }
+ )
+
+ expect(config.adapter_options[:encryption][:tls_options][:cert]).to be_a(OpenSSL::X509::Certificate)
+ expect(config.adapter_options[:encryption][:tls_options][:key]).to be_a(OpenSSL::PKey::RSA)
+ end
+
+ it 'logs an error when an invalid key or cert are configured' do
+ allow(Rails.logger).to receive(:error)
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'start_tls',
+ 'tls_options' => {
+ 'cert' => 'invalid cert',
+ 'key' => 'invalid_key'
+ }
+ }
+ )
+
+ config.adapter_options
+
+ expect(Rails.logger).to have_received(:error).with(/LDAP TLS Options/).twice
+ end
+
context 'when verify_certificates is enabled' do
it 'sets tls_options to OpenSSL defaults' do
stub_ldap_config(
@@ -130,7 +225,9 @@ describe Gitlab::Auth::LDAP::Config do
'host' => 'ldap.example.com',
'port' => 686,
'encryption' => 'simple_tls',
- 'ca_file' => '/etc/ca.pem'
+ 'tls_options' => {
+ 'ca_file' => '/etc/ca.pem'
+ }
}
)
@@ -145,7 +242,9 @@ describe Gitlab::Auth::LDAP::Config do
'host' => 'ldap.example.com',
'port' => 686,
'encryption' => 'simple_tls',
- 'ca_file' => ' '
+ 'tls_options' => {
+ 'ca_file' => ' '
+ }
}
)
@@ -160,7 +259,9 @@ describe Gitlab::Auth::LDAP::Config do
'host' => 'ldap.example.com',
'port' => 686,
'encryption' => 'simple_tls',
- 'ssl_version' => 'TLSv1_2'
+ 'tls_options' => {
+ 'ssl_version' => 'TLSv1_2'
+ }
}
)
@@ -175,7 +276,9 @@ describe Gitlab::Auth::LDAP::Config do
'host' => 'ldap.example.com',
'port' => 686,
'encryption' => 'simple_tls',
- 'ssl_version' => ' '
+ 'tls_options' => {
+ 'ssl_version' => ' '
+ }
}
)
@@ -223,6 +326,23 @@ describe Gitlab::Auth::LDAP::Config do
)
end
+ it 'transforms SSL cert and key to OpenSSL objects' do
+ stub_ldap_config(
+ options: {
+ 'host' => 'ldap.example.com',
+ 'port' => 686,
+ 'encryption' => 'start_tls',
+ 'tls_options' => {
+ 'cert' => raw_cert,
+ 'key' => raw_key
+ }
+ }
+ )
+
+ expect(config.omniauth_options[:tls_options][:cert]).to be_a(OpenSSL::X509::Certificate)
+ expect(config.omniauth_options[:tls_options][:key]).to be_a(OpenSSL::PKey::RSA)
+ end
+
context 'when verify_certificates is enabled' do
it 'specifies disable_verify_certificates as false' do
stub_ldap_config(
@@ -261,11 +381,13 @@ describe Gitlab::Auth::LDAP::Config do
'port' => 686,
'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'ca_file' => '/etc/ca.pem'
+ 'tls_options' => {
+ 'ca_file' => '/etc/ca.pem'
+ }
}
)
- expect(config.omniauth_options).to include({ ca_file: '/etc/ca.pem' })
+ expect(config.omniauth_options[:tls_options]).to include({ ca_file: '/etc/ca.pem' })
end
end
@@ -277,11 +399,13 @@ describe Gitlab::Auth::LDAP::Config do
'port' => 686,
'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'ca_file' => ' '
+ 'tls_options' => {
+ 'ca_file' => ' '
+ }
}
)
- expect(config.omniauth_options).not_to have_key(:ca_file)
+ expect(config.omniauth_options[:tls_options]).not_to have_key(:ca_file)
end
end
@@ -293,11 +417,13 @@ describe Gitlab::Auth::LDAP::Config do
'port' => 686,
'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'ssl_version' => 'TLSv1_2'
+ 'tls_options' => {
+ 'ssl_version' => 'TLSv1_2'
+ }
}
)
- expect(config.omniauth_options).to include({ ssl_version: 'TLSv1_2' })
+ expect(config.omniauth_options[:tls_options]).to include({ ssl_version: 'TLSv1_2' })
end
end
@@ -309,11 +435,14 @@ describe Gitlab::Auth::LDAP::Config do
'port' => 686,
'encryption' => 'simple_tls',
'verify_certificates' => true,
- 'ssl_version' => ' '
+ 'tls_options' => {
+ 'ssl_version' => ' '
+ }
}
)
- expect(config.omniauth_options).not_to have_key(:ssl_version)
+ # OpenSSL default params includes `ssl_version` so we just check that it's not blank
+ expect(config.omniauth_options[:tls_options]).not_to include({ ssl_version: ' ' })
end
end
end
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index dcbd12fe190..b765c265e69 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -207,6 +207,7 @@ describe Gitlab::Auth::OAuth::User do
before do
allow(ldap_user).to receive(:uid) { uid }
allow(ldap_user).to receive(:username) { uid }
+ allow(ldap_user).to receive(:name) { 'John Doe' }
allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
allow(ldap_user).to receive(:dn) { dn }
end
@@ -221,6 +222,7 @@ describe Gitlab::Auth::OAuth::User do
it "creates a user with dual LDAP and omniauth identities" do
expect(gl_user).to be_valid
expect(gl_user.username).to eql uid
+ expect(gl_user.name).to eql 'John Doe'
expect(gl_user.email).to eql 'johndoe@example.com'
expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
@@ -232,11 +234,13 @@ describe Gitlab::Auth::OAuth::User do
)
end
- it "has email set as synced" do
+ it "has name and email set as synced" do
+ expect(gl_user.user_synced_attributes_metadata.name_synced).to be_truthy
expect(gl_user.user_synced_attributes_metadata.email_synced).to be_truthy
end
- it "has email set as read-only" do
+ it "has name and email set as read-only" do
+ expect(gl_user.read_only_attribute?(:name)).to be_truthy
expect(gl_user.read_only_attribute?(:email)).to be_truthy
end
@@ -246,7 +250,7 @@ describe Gitlab::Auth::OAuth::User do
end
context "and LDAP user has an account already" do
- let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
+ let!(:existing_user) { create(:omniauth_user, name: 'John Doe', email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
it "adds the omniauth identity to the LDAP account" do
allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
@@ -254,6 +258,7 @@ describe Gitlab::Auth::OAuth::User do
expect(gl_user).to be_valid
expect(gl_user.username).to eql 'john'
+ expect(gl_user.name).to eql 'John Doe'
expect(gl_user.email).to eql 'john@example.com'
expect(gl_user.identities.length).to be 2
identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index a4a6338961e..3b5ca7c950c 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -5,7 +5,15 @@ describe Gitlab::Auth do
describe 'constants' do
it 'API_SCOPES contains all scopes for API access' do
- expect(subject::API_SCOPES).to eq %i[api read_user sudo read_repository]
+ expect(subject::API_SCOPES).to eq %i[api read_user]
+ end
+
+ it 'ADMIN_SCOPES contains all scopes for ADMIN access' do
+ expect(subject::ADMIN_SCOPES).to eq %i[sudo]
+ end
+
+ it 'REPOSITORY_SCOPES contains all scopes for REPOSITORY access' do
+ expect(subject::REPOSITORY_SCOPES).to eq %i[read_repository write_repository]
end
it 'OPENID_SCOPES contains all scopes for OpenID Connect' do
@@ -19,7 +27,29 @@ describe Gitlab::Auth do
it 'optional_scopes contains all non-default scopes' do
stub_container_registry_config(enabled: true)
- expect(subject.optional_scopes).to eq %i[read_user sudo read_repository read_registry openid profile email]
+ expect(subject.optional_scopes).to eq %i[read_user read_repository write_repository read_registry sudo openid profile email]
+ end
+ end
+
+ context 'available_scopes' do
+ it 'contains all non-default scopes' do
+ stub_container_registry_config(enabled: true)
+
+ expect(subject.all_available_scopes).to eq %i[api read_user read_repository write_repository read_registry sudo]
+ end
+
+ it 'contains for non-admin user all non-default scopes without ADMIN access' do
+ stub_container_registry_config(enabled: true)
+ user = create(:user, admin: false)
+
+ expect(subject.available_scopes_for(user)).to eq %i[api read_user read_repository write_repository read_registry]
+ end
+
+ it 'contains for admin user all non-default scopes with ADMIN access' do
+ stub_container_registry_config(enabled: true)
+ user = create(:user, admin: true)
+
+ expect(subject.available_scopes_for(user)).to eq %i[api read_user read_repository write_repository read_registry sudo]
end
context 'registry_scopes' do
@@ -122,7 +152,7 @@ describe Gitlab::Auth do
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_only_authentication_abilities))
end
it 'does not try password auth before oauth' do
@@ -150,7 +180,7 @@ describe Gitlab::Auth do
token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
- expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_only_authentication_abilities))
end
end
@@ -182,8 +212,19 @@ describe Gitlab::Auth do
it 'succeeds for personal access tokens with the `api` scope' do
personal_access_token = create(:personal_access_token, scopes: ['api'])
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, full_authentication_abilities))
+ expect_results_with_abilities(personal_access_token, full_authentication_abilities)
+ end
+
+ it 'succeeds for personal access tokens with the `read_repository` scope' do
+ personal_access_token = create(:personal_access_token, scopes: ['read_repository'])
+
+ expect_results_with_abilities(personal_access_token, [:download_code])
+ end
+
+ it 'succeeds for personal access tokens with the `write_repository` scope' do
+ personal_access_token = create(:personal_access_token, scopes: ['write_repository'])
+
+ expect_results_with_abilities(personal_access_token, [:download_code, :push_code])
end
context 'when registry is enabled' do
@@ -194,28 +235,24 @@ describe Gitlab::Auth do
it 'succeeds for personal access tokens with the `read_registry` scope' do
personal_access_token = create(:personal_access_token, scopes: ['read_registry'])
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, [:read_container_image]))
+ expect_results_with_abilities(personal_access_token, [:read_container_image])
end
end
it 'succeeds if it is an impersonation token' do
impersonation_token = create(:personal_access_token, :impersonation, scopes: ['api'])
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', impersonation_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(impersonation_token.user, nil, :personal_access_token, full_authentication_abilities))
+ expect_results_with_abilities(impersonation_token, full_authentication_abilities)
end
it 'limits abilities based on scope' do
personal_access_token = create(:personal_access_token, scopes: %w[read_user sudo])
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: '')
- expect(gl_auth.find_for_git_client('', personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(personal_access_token.user, nil, :personal_access_token, []))
+ expect_results_with_abilities(personal_access_token, [])
end
it 'fails if password is nil' do
- expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: '')
- expect(gl_auth.find_for_git_client('', nil, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+ expect_results_with_abilities(nil, nil, false)
end
end
@@ -479,7 +516,7 @@ describe Gitlab::Auth do
]
end
- def read_authentication_abilities
+ def read_only_authentication_abilities
[
:read_project,
:download_code,
@@ -488,7 +525,7 @@ describe Gitlab::Auth do
end
def read_write_authentication_abilities
- read_authentication_abilities + [
+ read_only_authentication_abilities + [
:push_code,
:create_container_image
]
@@ -499,4 +536,10 @@ describe Gitlab::Auth do
:admin_container_image
]
end
+
+ def expect_results_with_abilities(personal_access_token, abilities, success = true)
+ expect(gl_auth).to receive(:rate_limit!).with('ip', success: success, login: '')
+ expect(gl_auth.find_for_git_client('', personal_access_token&.token, project: nil, ip: 'ip'))
+ .to eq(Gitlab::Auth::Result.new(personal_access_token&.user, nil, personal_access_token.nil? ? nil : :personal_access_token, abilities))
+ end
end
diff --git a/spec/lib/gitlab/authorized_keys_spec.rb b/spec/lib/gitlab/authorized_keys_spec.rb
new file mode 100644
index 00000000000..42bc509eeef
--- /dev/null
+++ b/spec/lib/gitlab/authorized_keys_spec.rb
@@ -0,0 +1,194 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::AuthorizedKeys do
+ let(:logger) { double('logger').as_null_object }
+
+ subject { described_class.new(logger) }
+
+ describe '#add_key' do
+ context 'authorized_keys file exists' do
+ before do
+ create_authorized_keys_fixture
+ end
+
+ after do
+ delete_authorized_keys_file
+ end
+
+ it "adds a line at the end of the file and strips trailing garbage" do
+ auth_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E"
+
+ expect(logger).to receive(:info).with('Adding key (key-741): ssh-rsa AAAAB3NzaDAxx2E')
+ expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E trailing garbage'))
+ .to be_truthy
+ expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line}\n")
+ end
+ end
+
+ context 'authorized_keys file does not exist' do
+ before do
+ delete_authorized_keys_file
+ end
+
+ it 'creates the file' do
+ expect(subject.add_key('key-741', 'ssh-rsa AAAAB3NzaDAxx2E')).to be_truthy
+ expect(File.exist?(tmp_authorized_keys_path)).to be_truthy
+ end
+ end
+ end
+
+ describe '#batch_add_keys' do
+ let(:keys) do
+ [
+ double(shell_id: 'key-12', key: 'ssh-dsa ASDFASGADG trailing garbage'),
+ double(shell_id: 'key-123', key: 'ssh-rsa GFDGDFSGSDFG')
+ ]
+ end
+
+ context 'authorized_keys file exists' do
+ before do
+ create_authorized_keys_fixture
+ end
+
+ after do
+ delete_authorized_keys_file
+ end
+
+ it "adds lines at the end of the file" do
+ auth_line1 = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-12\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-dsa ASDFASGADG"
+ auth_line2 = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-123\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa GFDGDFSGSDFG"
+
+ expect(logger).to receive(:info).with('Adding key (key-12): ssh-dsa ASDFASGADG')
+ expect(logger).to receive(:info).with('Adding key (key-123): ssh-rsa GFDGDFSGSDFG')
+ expect(subject.batch_add_keys(keys)).to be_truthy
+ expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{auth_line1}\n#{auth_line2}\n")
+ end
+
+ context "invalid key" do
+ let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] }
+
+ it "doesn't add keys" do
+ expect(subject.batch_add_keys(keys)).to be_falsey
+ expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n")
+ end
+ end
+ end
+
+ context 'authorized_keys file does not exist' do
+ before do
+ delete_authorized_keys_file
+ end
+
+ it 'creates the file' do
+ expect(subject.batch_add_keys(keys)).to be_truthy
+ expect(File.exist?(tmp_authorized_keys_path)).to be_truthy
+ end
+ end
+ end
+
+ describe '#rm_key' do
+ context 'authorized_keys file exists' do
+ before do
+ create_authorized_keys_fixture
+ end
+
+ after do
+ delete_authorized_keys_file
+ end
+
+ it "removes the right line" do
+ other_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-742\",options ssh-rsa AAAAB3NzaDAxx2E"
+ delete_line = "command=\"#{Gitlab.config.gitlab_shell.path}/bin/gitlab-shell key-741\",options ssh-rsa AAAAB3NzaDAxx2E"
+ erased_line = delete_line.gsub(/./, '#')
+ File.open(tmp_authorized_keys_path, 'a') do |auth_file|
+ auth_file.puts delete_line
+ auth_file.puts other_line
+ end
+
+ expect(logger).to receive(:info).with('Removing key (key-741)')
+ expect(subject.rm_key('key-741')).to be_truthy
+ expect(File.read(tmp_authorized_keys_path)).to eq("existing content\n#{erased_line}\n#{other_line}\n")
+ end
+ end
+
+ context 'authorized_keys file does not exist' do
+ before do
+ delete_authorized_keys_file
+ end
+
+ it 'returns false' do
+ expect(subject.rm_key('key-741')).to be_falsey
+ end
+ end
+ end
+
+ describe '#clear' do
+ context 'authorized_keys file exists' do
+ before do
+ create_authorized_keys_fixture
+ end
+
+ after do
+ delete_authorized_keys_file
+ end
+
+ it "returns true" do
+ expect(subject.clear).to be_truthy
+ end
+ end
+
+ context 'authorized_keys file does not exist' do
+ before do
+ delete_authorized_keys_file
+ end
+
+ it "still returns true" do
+ expect(subject.clear).to be_truthy
+ end
+ end
+ end
+
+ describe '#list_key_ids' do
+ context 'authorized_keys file exists' do
+ before do
+ create_authorized_keys_fixture(
+ existing_content:
+ "key-1\tssh-dsa AAA\nkey-2\tssh-rsa BBB\nkey-3\tssh-rsa CCC\nkey-9000\tssh-rsa DDD\n"
+ )
+ end
+
+ after do
+ delete_authorized_keys_file
+ end
+
+ it 'returns array of key IDs' do
+ expect(subject.list_key_ids).to eq([1, 2, 3, 9000])
+ end
+ end
+
+ context 'authorized_keys file does not exist' do
+ before do
+ delete_authorized_keys_file
+ end
+
+ it 'returns an empty array' do
+ expect(subject.list_key_ids).to be_empty
+ end
+ end
+ end
+
+ def create_authorized_keys_fixture(existing_content: 'existing content')
+ FileUtils.mkdir_p(File.dirname(tmp_authorized_keys_path))
+ File.open(tmp_authorized_keys_path, 'w') { |file| file.puts(existing_content) }
+ end
+
+ def delete_authorized_keys_file
+ File.delete(tmp_authorized_keys_path) if File.exist?(tmp_authorized_keys_path)
+ end
+
+ def tmp_authorized_keys_path
+ Gitlab.config.gitlab_shell.authorized_keys_file
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
index 27281333348..0a5b99d27e7 100644
--- a/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_diff_files_spec.rb
@@ -3,6 +3,12 @@ require 'spec_helper'
# rubocop:disable RSpec/FactoriesInMigrationSpecs
describe Gitlab::BackgroundMigration::DeleteDiffFiles, :migration, :sidekiq, schema: 20180619121030 do
describe '#perform' do
+ before do
+ # This migration was created before we introduced ProjectCiCdSetting#default_git_depth
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0)
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil)
+ end
+
context 'when diff files can be deleted' do
let(:merge_request) { create(:merge_request, :merged) }
let!(:merge_request_diff) do
diff --git a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
index bc71a90605a..d3f7f1ded16 100644
--- a/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
+++ b/spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb
@@ -172,14 +172,12 @@ describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :m
let(:exception) { ActiveRecord::RecordNotFound }
let(:perform_ignoring_exceptions) do
- begin
- subject.perform(start_id, stop_id)
- rescue described_class::Error
- end
+ subject.perform(start_id, stop_id)
+ rescue described_class::Error
end
before do
- allow_any_instance_of(described_class::MergeRequestDiff::ActiveRecord_Relation)
+ allow_any_instance_of(ActiveRecord::Relation)
.to receive(:update_all).and_raise(exception)
end
diff --git a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
index 7c7e58d6bb7..582396275ed 100644
--- a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
@@ -51,7 +51,7 @@ describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 201
statuses[:pending]]
end
- it 'recovers from unique constraint violation only twice' do
+ it 'recovers from unique constraint violation only twice', :quarantine do
allow(described_class::Migratable::Stage)
.to receive(:find_by).and_return(nil)
diff --git a/spec/lib/gitlab/background_migration/migrate_legacy_uploads_spec.rb b/spec/lib/gitlab/background_migration/migrate_legacy_uploads_spec.rb
new file mode 100644
index 00000000000..16b9de6a84e
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_legacy_uploads_spec.rb
@@ -0,0 +1,253 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MigrateLegacyUploads, :migration, schema: 20190103140724 do
+ let(:test_dir) { FileUploader.options['storage_path'] }
+
+ # rubocop: disable RSpec/FactoriesInMigrationSpecs
+ let(:namespace) { create(:namespace) }
+ let(:project) { create(:project, :legacy_storage, namespace: namespace) }
+ let(:issue) { create(:issue, project: project) }
+
+ let(:note1) { create(:note, note: 'some note text awesome', project: project, noteable: issue) }
+ let(:note2) { create(:note, note: 'some note', project: project, noteable: issue) }
+
+ let(:hashed_project) { create(:project, namespace: namespace) }
+ let(:issue_hashed_project) { create(:issue, project: hashed_project) }
+ let(:note_hashed_project) { create(:note, note: 'some note', project: hashed_project, attachment: 'text.pdf', noteable: issue_hashed_project) }
+
+ let(:standard_upload) do
+ create(:upload,
+ path: "secretabcde/image.png",
+ model_id: create(:project).id, model_type: 'Project', uploader: 'FileUploader')
+ end
+
+ before do
+ # This migration was created before we introduced ProjectCiCdSetting#default_git_depth
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil)
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0)
+
+ namespace
+ project
+ issue
+ note1
+ note2
+ hashed_project
+ issue_hashed_project
+ note_hashed_project
+ standard_upload
+ end
+
+ def create_remote_upload(model, filename)
+ create(:upload, :attachment_upload,
+ path: "note/attachment/#{model.id}/#{filename}", secret: nil,
+ store: ObjectStorage::Store::REMOTE, model: model)
+ end
+
+ def create_upload(model, filename, with_file = true)
+ params = {
+ path: "uploads/-/system/note/attachment/#{model.id}/#{filename}",
+ model: model,
+ store: ObjectStorage::Store::LOCAL
+ }
+
+ upload = if with_file
+ create(:upload, :with_file, :attachment_upload, params)
+ else
+ create(:upload, :attachment_upload, params)
+ end
+
+ model.update(attachment: upload.build_uploader)
+ model.attachment.upload
+ end
+
+ let(:start_id) { 1 }
+ let(:end_id) { 10000 }
+
+ def new_upload_legacy
+ Upload.find_by(model_id: project.id, model_type: 'Project')
+ end
+
+ def new_upload_hashed
+ Upload.find_by(model_id: hashed_project.id, model_type: 'Project')
+ end
+
+ shared_examples 'migrates files correctly' do
+ before do
+ described_class.new.perform(start_id, end_id)
+ end
+
+ it 'removes all the legacy upload records' do
+ expect(Upload.where(uploader: 'AttachmentUploader')).to be_empty
+
+ expect(standard_upload.reload).to eq(standard_upload)
+ end
+
+ it 'creates new upload records correctly' do
+ expect(new_upload_legacy.secret).not_to be_nil
+ expect(new_upload_legacy.path).to end_with("#{new_upload_legacy.secret}/image.png")
+ expect(new_upload_legacy.model_id).to eq(project.id)
+ expect(new_upload_legacy.model_type).to eq('Project')
+ expect(new_upload_legacy.uploader).to eq('FileUploader')
+
+ expect(new_upload_hashed.secret).not_to be_nil
+ expect(new_upload_hashed.path).to end_with("#{new_upload_hashed.secret}/text.pdf")
+ expect(new_upload_hashed.model_id).to eq(hashed_project.id)
+ expect(new_upload_hashed.model_type).to eq('Project')
+ expect(new_upload_hashed.uploader).to eq('FileUploader')
+ end
+
+ it 'updates the legacy upload notes so that they include the file references in the markdown' do
+ expected_path = File.join('/uploads', new_upload_legacy.secret, 'image.png')
+ expected_markdown = "some note text awesome \n ![image](#{expected_path})"
+ expect(note1.reload.note).to eq(expected_markdown)
+
+ expected_path = File.join('/uploads', new_upload_hashed.secret, 'text.pdf')
+ expected_markdown = "some note \n [text.pdf](#{expected_path})"
+ expect(note_hashed_project.reload.note).to eq(expected_markdown)
+ end
+
+ it 'removed the attachments from the note model' do
+ expect(note1.reload.attachment.file).to be_nil
+ expect(note2.reload.attachment.file).to be_nil
+ expect(note_hashed_project.reload.attachment.file).to be_nil
+ end
+ end
+
+ context 'when legacy uploads are stored in local storage' do
+ let!(:legacy_upload1) { create_upload(note1, 'image.png') }
+ let!(:legacy_upload_not_found) { create_upload(note2, 'image.png', false) }
+ let!(:legacy_upload_hashed) { create_upload(note_hashed_project, 'text.pdf', with_file: true) }
+
+ shared_examples 'removes legacy local files' do
+ it 'removes all the legacy upload records' do
+ expect(File.exist?(legacy_upload1.absolute_path)).to be_truthy
+ expect(File.exist?(legacy_upload_hashed.absolute_path)).to be_truthy
+
+ described_class.new.perform(start_id, end_id)
+
+ expect(File.exist?(legacy_upload1.absolute_path)).to be_falsey
+ expect(File.exist?(legacy_upload_hashed.absolute_path)).to be_falsey
+ end
+ end
+
+ context 'when object storage is disabled for FileUploader' do
+ it_behaves_like 'migrates files correctly'
+ it_behaves_like 'removes legacy local files'
+
+ it 'moves legacy uploads to the correct location' do
+ described_class.new.perform(start_id, end_id)
+
+ expected_path1 = File.join(test_dir, 'uploads', namespace.path, project.path, new_upload_legacy.secret, 'image.png')
+ expected_path2 = File.join(test_dir, 'uploads', hashed_project.disk_path, new_upload_hashed.secret, 'text.pdf')
+
+ expect(File.exist?(expected_path1)).to be_truthy
+ expect(File.exist?(expected_path2)).to be_truthy
+ end
+
+ context 'when the upload move fails' do
+ it 'does not remove old uploads' do
+ expect(FileUploader).to receive(:copy_to).twice.and_raise('failed')
+
+ described_class.new.perform(start_id, end_id)
+
+ expect(legacy_upload1.reload).to eq(legacy_upload1)
+ expect(legacy_upload_hashed.reload).to eq(legacy_upload_hashed)
+ expect(standard_upload.reload).to eq(standard_upload)
+ end
+ end
+ end
+
+ context 'when object storage is enabled for FileUploader' do
+ before do
+ stub_uploads_object_storage(FileUploader)
+ end
+
+ it_behaves_like 'migrates files correctly'
+ it_behaves_like 'removes legacy local files'
+
+ # The process of migrating to object storage is a manual one,
+ # so it would go against expectations to automatically migrate these files
+ # to object storage during this migration.
+ # After this migration, these files should be able to successfully migrate to object storage.
+ it 'stores files locally' do
+ described_class.new.perform(start_id, end_id)
+
+ expected_path1 = File.join(test_dir, 'uploads', namespace.path, project.path, new_upload_legacy.secret, 'image.png')
+ expected_path2 = File.join(test_dir, 'uploads', hashed_project.disk_path, new_upload_hashed.secret, 'text.pdf')
+
+ expect(File.exist?(expected_path1)).to be_truthy
+ expect(File.exist?(expected_path2)).to be_truthy
+ end
+ end
+
+ context 'with legacy_diff_note upload' do
+ let!(:merge_request) { create(:merge_request, source_project: project) }
+ let!(:legacy_diff_note) { create(:legacy_diff_note_on_merge_request, note: 'some note', project: project, noteable: merge_request) }
+ let!(:legacy_upload_diff_note) do
+ create(:upload, :with_file, :attachment_upload,
+ path: "uploads/-/system/note/attachment/#{legacy_diff_note.id}/some_legacy.pdf", model: legacy_diff_note)
+ end
+
+ before do
+ described_class.new.perform(start_id, end_id)
+ end
+
+ it 'does not remove legacy diff note file' do
+ expect(File.exist?(legacy_upload_diff_note.absolute_path)).to be_truthy
+ end
+
+ it 'removes all the legacy upload records except for the one with legacy_diff_note' do
+ expect(Upload.where(uploader: 'AttachmentUploader')).to eq([legacy_upload_diff_note])
+ end
+
+ it 'adds link to the troubleshooting documentation to the note' do
+ help_doc_link = 'https://docs.gitlab.com/ee/administration/troubleshooting/migrations.html#legacy-upload-migration'
+
+ expect(legacy_diff_note.reload.note).to include(help_doc_link)
+ end
+ end
+ end
+
+ context 'when legacy uploads are stored in object storage' do
+ let!(:legacy_upload1) { create_remote_upload(note1, 'image.png') }
+ let!(:legacy_upload_not_found) { create_remote_upload(note2, 'non-existing.pdf') }
+ let!(:legacy_upload_hashed) { create_remote_upload(note_hashed_project, 'text.pdf') }
+ let(:remote_files) do
+ [
+ { key: "#{legacy_upload1.path}" },
+ { key: "#{legacy_upload_hashed.path}" }
+ ]
+ end
+ let(:connection) { ::Fog::Storage.new(FileUploader.object_store_credentials) }
+ let(:bucket) { connection.directories.create(key: 'uploads') }
+
+ def create_remote_files
+ remote_files.each { |file| bucket.files.create(file) }
+ end
+
+ before do
+ stub_uploads_object_storage(FileUploader)
+ create_remote_files
+ end
+
+ it_behaves_like 'migrates files correctly'
+
+ it 'moves legacy uploads to the correct remote location' do
+ described_class.new.perform(start_id, end_id)
+
+ connection = ::Fog::Storage.new(FileUploader.object_store_credentials)
+ expect(connection.get_object('uploads', new_upload_legacy.path)[:status]).to eq(200)
+ expect(connection.get_object('uploads', new_upload_hashed.path)[:status]).to eq(200)
+ end
+
+ it 'removes all the legacy upload records' do
+ described_class.new.perform(start_id, end_id)
+
+ remote_files.each do |remote_file|
+ expect(bucket.files.get(remote_file[:key])).to be_nil
+ end
+ end
+ end
+ # rubocop: enable RSpec/FactoriesInMigrationSpecs
+end
diff --git a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
index f8107dd40b9..4db829b1e7b 100644
--- a/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_stage_index_spec.rb
@@ -30,6 +30,6 @@ describe Gitlab::BackgroundMigration::MigrateStageIndex, :migration, schema: 201
described_class.new.perform(100, 101)
- expect(stages.all.pluck(:position)).to eq [2, 3]
+ expect(stages.all.order(:id).pluck(:position)).to eq [2, 3]
end
end
diff --git a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb
index 812e0cc6947..128e118ac17 100644
--- a/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_cluster_kubernetes_namespace_table_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :
end
shared_examples 'consistent kubernetes namespace attributes' do
- it 'should populate namespace and service account information' do
+ it 'populates namespace and service account information' do
migration.perform
clusters_with_namespace.each do |cluster|
@@ -41,7 +41,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :
context 'when no Clusters::Project has a Clusters::KubernetesNamespace' do
let(:cluster_projects) { cluster_projects_table.all }
- it 'should create a Clusters::KubernetesNamespace per Clusters::Project' do
+ it 'creates a Clusters::KubernetesNamespace per Clusters::Project' do
expect do
migration.perform
end.to change(Clusters::KubernetesNamespace, :count).by(cluster_projects_table.count)
@@ -57,7 +57,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :
create_kubernetes_namespace(clusters_table.all)
end
- it 'should not create any Clusters::KubernetesNamespace' do
+ it 'does not create any Clusters::KubernetesNamespace' do
expect do
migration.perform
end.not_to change(Clusters::KubernetesNamespace, :count)
@@ -78,7 +78,7 @@ describe Gitlab::BackgroundMigration::PopulateClusterKubernetesNamespaceTable, :
end.to change(Clusters::KubernetesNamespace, :count).by(with_no_kubernetes_namespace.count)
end
- it 'should not modify clusters with Clusters::KubernetesNamespace' do
+ it 'does not modify clusters with Clusters::KubernetesNamespace' do
migration.perform
with_kubernetes_namespace.each do |cluster|
diff --git a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb
index 3e009fed0f1..c6bc3db88a3 100644
--- a/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_external_pipeline_source_spec.rb
@@ -9,6 +9,9 @@ describe Gitlab::BackgroundMigration::PopulateExternalPipelineSource, :migration
before do
# This migration was created before we introduced metadata configs
stub_feature_flags(ci_build_metadata_config: false)
+ # This migration was created before we introduced ProjectCiCdSetting#default_git_depth
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil)
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0)
end
let!(:internal_pipeline) { create(:ci_pipeline, source: :web) }
diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
index 8582af96199..0e73c8c59c9 100644
--- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
+++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch
migration.perform(1, 3)
end
- it 'it creates the fork network' do
+ it 'creates the fork network' do
expect(fork_network1).not_to be_nil
expect(fork_network2).not_to be_nil
end
diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb
new file mode 100644
index 00000000000..4a81a37d341
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/populate_merge_request_assignees_table_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::BackgroundMigration::PopulateMergeRequestAssigneesTable, :migration, schema: 20190315191339 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:users) { table(:users) }
+
+ let(:user) { users.create!(email: 'test@example.com', projects_limit: 100, username: 'test') }
+ let(:user_2) { users.create!(email: 'test2@example.com', projects_limit: 100, username: 'test') }
+ let(:user_3) { users.create!(email: 'test3@example.com', projects_limit: 100, username: 'test') }
+
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:merge_request_assignees) { table(:merge_request_assignees) }
+
+ def create_merge_request(id, params = {})
+ params.merge!(id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}")
+
+ merge_requests.create(params)
+ end
+
+ describe '#perform' do
+ it 'creates merge_request_assignees rows according to merge_requests' do
+ create_merge_request(2, assignee_id: user.id)
+ create_merge_request(3, assignee_id: user_2.id)
+ create_merge_request(4, assignee_id: user_3.id)
+ # Test filtering already migrated row
+ merge_request_assignees.create!(merge_request_id: 2, user_id: user_3.id)
+
+ subject.perform(1, 4)
+
+ rows = merge_request_assignees.order(:id).map { |row| row.attributes.slice('merge_request_id', 'user_id') }
+ existing_rows = [
+ { 'merge_request_id' => 2, 'user_id' => user_3.id }
+ ]
+ created_rows = [
+ { 'merge_request_id' => 3, 'user_id' => user_2.id },
+ { 'merge_request_id' => 4, 'user_id' => user_3.id }
+ ]
+ expected_rows = existing_rows + created_rows
+
+ expect(rows.size).to eq(expected_rows.size)
+ expected_rows.each do |expected_row|
+ expect(rows).to include(expected_row)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
new file mode 100644
index 00000000000..740781f1aa5
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/reset_merge_status_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::BackgroundMigration::ResetMergeStatus, :migration, schema: 20190528180441 do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id, extra_params = {})
+ params = {
+ id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}"
+ }.merge(extra_params)
+
+ merge_requests.create!(params)
+ end
+
+ it 'correctly updates opened mergeable MRs to unchecked' do
+ create_merge_request(1, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(2, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(3, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(4, state: 'merged', merge_status: 'can_be_merged')
+ create_merge_request(5, state: 'opened', merge_status: 'cannot_be_merged')
+
+ subject.perform(1, 5)
+
+ expected_rows = [
+ { id: 1, state: 'opened', merge_status: 'unchecked' },
+ { id: 2, state: 'opened', merge_status: 'unchecked' },
+ { id: 3, state: 'opened', merge_status: 'unchecked' },
+ { id: 4, state: 'merged', merge_status: 'can_be_merged' },
+ { id: 5, state: 'opened', merge_status: 'cannot_be_merged' }
+ ]
+
+ rows = merge_requests.order(:id).map do |row|
+ row.attributes.slice('id', 'state', 'merge_status').symbolize_keys
+ end
+
+ expect(rows).to eq(expected_rows)
+ end
+end
diff --git a/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
new file mode 100644
index 00000000000..d494ce68c5b
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/schedule_calculate_wiki_sizes_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190527194900_schedule_calculate_wiki_sizes.rb')
+
+describe ScheduleCalculateWikiSizes, :migration, :sidekiq do
+ let(:migration_class) { Gitlab::BackgroundMigration::CalculateWikiSizes }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:project_statistics) { table(:project_statistics) }
+
+ context 'when missing wiki sizes exist' do
+ before do
+ namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration')
+ projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1)
+ projects.create!(id: 2, name: 'wiki-project-2', path: 'wiki-project-2', namespace_id: 1)
+ projects.create!(id: 3, name: 'wiki-project-3', path: 'wiki-project-3', namespace_id: 1)
+ project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000)
+ project_statistics.create!(id: 2, project_id: 2, namespace_id: 1, wiki_size: nil)
+ project_statistics.create!(id: 3, project_id: 3, namespace_id: 1, wiki_size: nil)
+ end
+
+ it 'schedules a background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration_name).to be_scheduled_delayed_migration(5.minutes, 2, 3)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 1
+ end
+ end
+ end
+
+ it 'calculates missing wiki sizes' do
+ expect(project_statistics.find_by(id: 2).wiki_size).to be_nil
+ expect(project_statistics.find_by(id: 3).wiki_size).to be_nil
+
+ migrate!
+
+ expect(project_statistics.find_by(id: 2).wiki_size).not_to be_nil
+ expect(project_statistics.find_by(id: 3).wiki_size).not_to be_nil
+ end
+ end
+
+ context 'when missing wiki sizes do not exist' do
+ before do
+ namespaces.create!(id: 1, name: 'wiki-migration', path: 'wiki-migration')
+ projects.create!(id: 1, name: 'wiki-project-1', path: 'wiki-project-1', namespace_id: 1)
+ project_statistics.create!(id: 1, project_id: 1, namespace_id: 1, wiki_size: 1000)
+ end
+
+ it 'does not schedule a background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq 0
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/background_migration_spec.rb b/spec/lib/gitlab/background_migration_spec.rb
index 7d3d8a949ef..1d0ffb5e9df 100644
--- a/spec/lib/gitlab/background_migration_spec.rb
+++ b/spec/lib/gitlab/background_migration_spec.rb
@@ -195,4 +195,44 @@ describe Gitlab::BackgroundMigration do
end
end
end
+
+ describe '.dead_jobs?' do
+ let(:queue) do
+ [double(args: ['Foo', [10, 20]], queue: described_class.queue)]
+ 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 '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]], queue: described_class.queue)]
+ 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
+ end
+ end
end
diff --git a/spec/lib/gitlab/badge/pipeline/template_spec.rb b/spec/lib/gitlab/badge/pipeline/template_spec.rb
index 20fa4f879c3..bcef0b7e120 100644
--- a/spec/lib/gitlab/badge/pipeline/template_spec.rb
+++ b/spec/lib/gitlab/badge/pipeline/template_spec.rb
@@ -59,6 +59,16 @@ describe Gitlab::Badge::Pipeline::Template do
end
end
+ context 'when status is preparing' do
+ before do
+ allow(badge).to receive(:status).and_return('preparing')
+ end
+
+ it 'has expected color' do
+ expect(template.value_color).to eq '#dfb317'
+ end
+ end
+
context 'when status is unknown' do
before do
allow(badge).to receive(:status).and_return('unknown')
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index c432cc223b9..2e90f6c7f71 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::BitbucketImport::Importer do
before do
stub_omniauth_provider('bitbucket')
+ stub_feature_flags(stricter_mr_branch_name: false)
end
let(:statuses) do
@@ -95,6 +96,9 @@ describe Gitlab::BitbucketImport::Importer do
subject { described_class.new(project) }
describe '#import_pull_requests' do
+ let(:source_branch_sha) { sample.commits.last }
+ let(:target_branch_sha) { sample.commits.first }
+
before do
allow(subject).to receive(:import_wiki)
allow(subject).to receive(:import_issues)
@@ -102,9 +106,9 @@ describe Gitlab::BitbucketImport::Importer do
pull_request = instance_double(
Bitbucket::Representation::PullRequest,
iid: 10,
- source_branch_sha: sample.commits.last,
+ source_branch_sha: source_branch_sha,
source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch,
- target_branch_sha: sample.commits.first,
+ target_branch_sha: target_branch_sha,
target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch,
title: 'This is a title',
description: 'This is a test pull request',
@@ -162,6 +166,19 @@ describe Gitlab::BitbucketImport::Importer do
expect(reply_note).to be_a(DiffNote)
expect(reply_note.note).to eq(@reply.note)
end
+
+ context "when branches' sha is not found in the repository" do
+ let(:source_branch_sha) { 'a' * Commit::MIN_SHA_LENGTH }
+ let(:target_branch_sha) { 'b' * Commit::MIN_SHA_LENGTH }
+
+ it 'uses the pull request sha references' do
+ expect { subject.execute }.to change { MergeRequest.count }.by(1)
+
+ merge_request_diff = MergeRequest.first.merge_request_diff
+ expect(merge_request_diff.head_commit_sha).to eq source_branch_sha
+ expect(merge_request_diff.start_commit_sha).to eq target_branch_sha
+ end
+ end
end
context 'issues statuses' do
@@ -206,6 +223,46 @@ describe Gitlab::BitbucketImport::Importer do
body: {}.to_json)
end
+ context 'creating labels on project' do
+ before do
+ allow(importer).to receive(:import_wiki)
+ end
+
+ it 'creates labels as expected' do
+ expect { importer.execute }.to change { Label.count }.from(0).to(Gitlab::BitbucketImport::Importer::LABELS.size)
+ end
+
+ it 'does not fail if label is already existing' do
+ label = Gitlab::BitbucketImport::Importer::LABELS.first
+ ::Labels::CreateService.new(label).execute(project: project)
+
+ expect { importer.execute }.not_to raise_error
+ end
+
+ it 'does not create new labels' do
+ Gitlab::BitbucketImport::Importer::LABELS.each do |label|
+ create(:label, project: project, title: label[:title])
+ end
+
+ expect { importer.execute }.not_to change { Label.count }
+ end
+
+ it 'does not update existing ones' do
+ label_title = Gitlab::BitbucketImport::Importer::LABELS.first[:title]
+ existing_label = create(:label, project: project, title: label_title)
+ # Reload label from database so we avoid timestamp comparison issues related to time precision when comparing
+ # attributes later.
+ existing_label.reload
+
+ Timecop.freeze(Time.now + 1.minute) do
+ importer.execute
+
+ label_after_import = project.labels.find(existing_label.id)
+ expect(label_after_import.attributes).to eq(existing_label.attributes)
+ end
+ end
+ end
+
it 'maps statuses to open or closed' do
allow(importer).to receive(:import_wiki)
diff --git a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
index 1e90a2ef27f..cc09804fd53 100644
--- a/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_server_import/importer_spec.rb
@@ -105,6 +105,7 @@ describe Gitlab::BitbucketServerImport::Importer do
expect(merge_request.metrics.merged_by).to eq(project.owner)
expect(merge_request.metrics.merged_at).to eq(@merge_event.merge_timestamp)
expect(merge_request.merge_commit_sha).to eq('12345678')
+ expect(merge_request.state_id).to eq(3)
end
it 'imports comments' do
diff --git a/spec/lib/gitlab/checks/branch_check_spec.rb b/spec/lib/gitlab/checks/branch_check_spec.rb
index 77366e91dca..71b64a3b9df 100644
--- a/spec/lib/gitlab/checks/branch_check_spec.rb
+++ b/spec/lib/gitlab/checks/branch_check_spec.rb
@@ -48,10 +48,118 @@ describe Gitlab::Checks::BranchCheck do
context 'when project repository is empty' do
let(:project) { create(:project) }
- it 'raises an error if the user is not allowed to push to protected branches' do
- expect(user_access).to receive(:can_push_to_branch?).and_return(false)
+ context 'user is not allowed to push to protected branches' do
+ before do
+ allow(user_access)
+ .to receive(:can_push_to_branch?)
+ .and_return(false)
+ end
+
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/)
+ end
+ end
+
+ context 'user is allowed to push to protected branches' do
+ before do
+ allow(user_access)
+ .to receive(:can_push_to_branch?)
+ .and_return(true)
+ end
+
+ it 'allows branch creation' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+ end
+
+ context 'branch creation' do
+ let(:oldrev) { '0000000000000000000000000000000000000000' }
+ let(:ref) { 'refs/heads/feature' }
+
+ context 'user can push to branch' do
+ before do
+ allow(user_access)
+ .to receive(:can_push_to_branch?)
+ .with('feature')
+ .and_return(true)
+ end
- expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, /Ask a project Owner or Maintainer to create a default branch/)
+ it 'does not raise an error' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+
+ context 'user cannot push to branch' do
+ before do
+ allow(user_access)
+ .to receive(:can_push_to_branch?)
+ .with('feature')
+ .and_return(false)
+ end
+
+ context 'user cannot merge to branch' do
+ before do
+ allow(user_access)
+ .to receive(:can_merge_to_branch?)
+ .with('feature')
+ .and_return(false)
+ end
+
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to create protected branches on this project.')
+ end
+ end
+
+ context 'user can merge to branch' do
+ before do
+ allow(user_access)
+ .to receive(:can_merge_to_branch?)
+ .with('feature')
+ .and_return(true)
+
+ allow(project.repository)
+ .to receive(:branch_names_contains_sha)
+ .with(newrev)
+ .and_return(['branch'])
+ end
+
+ context "newrev isn't in any protected branches" do
+ before do
+ allow(ProtectedBranch)
+ .to receive(:any_protected?)
+ .with(project, ['branch'])
+ .and_return(false)
+ end
+
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only use an existing protected branch ref as the basis of a new protected branch.')
+ end
+ end
+
+ context 'newrev is included in a protected branch' do
+ before do
+ allow(ProtectedBranch)
+ .to receive(:any_protected?)
+ .with(project, ['branch'])
+ .and_return(true)
+ end
+
+ context 'via web interface' do
+ let(:protocol) { 'web' }
+
+ it 'allows branch creation' do
+ expect { subject.validate! }.not_to raise_error
+ end
+ end
+
+ context 'via SSH' do
+ it 'raises an error' do
+ expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You can only create protected branches using the web interface and API.')
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/checks/lfs_check_spec.rb b/spec/lib/gitlab/checks/lfs_check_spec.rb
index 35f8069c8a4..dad14e100a7 100644
--- a/spec/lib/gitlab/checks/lfs_check_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_check_spec.rb
@@ -27,6 +27,18 @@ describe Gitlab::Checks::LfsCheck do
allow(project).to receive(:lfs_enabled?).and_return(true)
end
+ context 'with lfs_check feature disabled' do
+ before do
+ stub_feature_flags(lfs_check: false)
+ end
+
+ it 'skips integrity check' do
+ expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
+
+ subject.validate!
+ end
+ end
+
context 'deletion' do
let(:changes) { { oldrev: oldrev, ref: ref } }
diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb
index 773a52cdfbc..6e20e0ef5c3 100644
--- a/spec/lib/gitlab/ci/build/image_spec.rb
+++ b/spec/lib/gitlab/ci/build/image_spec.rb
@@ -18,11 +18,16 @@ describe Gitlab::Ci::Build::Image do
it 'populates fabricated object with the proper name attribute' do
expect(subject.name).to eq(image_name)
end
+
+ it 'does not populate the ports' do
+ expect(subject.ports).to be_empty
+ end
end
context 'when image is defined as hash' do
let(:entrypoint) { '/bin/sh' }
- let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint } } ) }
+
+ let(:job) { create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80] } } ) }
it 'fabricates an object of the proper class' do
is_expected.to be_kind_of(described_class)
@@ -32,6 +37,13 @@ describe Gitlab::Ci::Build::Image do
expect(subject.name).to eq(image_name)
expect(subject.entrypoint).to eq(entrypoint)
end
+
+ it 'populates the ports' do
+ port = subject.ports.first
+ expect(port.number).to eq 80
+ expect(port.protocol).to eq 'http'
+ expect(port.name).to eq 'default_port'
+ end
end
context 'when image name is empty' do
@@ -67,6 +79,10 @@ describe Gitlab::Ci::Build::Image do
expect(subject.first).to be_kind_of(described_class)
expect(subject.first.name).to eq(service_image_name)
end
+
+ it 'does not populate the ports' do
+ expect(subject.first.ports).to be_empty
+ end
end
context 'when service is defined as hash' do
@@ -75,7 +91,7 @@ describe Gitlab::Ci::Build::Image do
let(:service_command) { 'sleep 30' }
let(:job) do
create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint,
- alias: service_alias, command: service_command }] })
+ alias: service_alias, command: service_command, ports: [80] }] })
end
it 'fabricates an non-empty array of objects' do
@@ -89,6 +105,11 @@ describe Gitlab::Ci::Build::Image do
expect(subject.first.entrypoint).to eq(service_entrypoint)
expect(subject.first.alias).to eq(service_alias)
expect(subject.first.command).to eq(service_command)
+
+ port = subject.first.ports.first
+ expect(port.number).to eq 80
+ expect(port.protocol).to eq 'http'
+ expect(port.name).to eq 'default_port'
end
end
diff --git a/spec/lib/gitlab/ci/build/policy/changes_spec.rb b/spec/lib/gitlab/ci/build/policy/changes_spec.rb
index dc3329061d1..92cf0376c02 100644
--- a/spec/lib/gitlab/ci/build/policy/changes_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/changes_spec.rb
@@ -133,7 +133,7 @@ describe Gitlab::Ci::Build::Policy::Changes do
let(:seed) { double('build seed', to_resource: ci_build) }
context 'when source is merge request' do
- let(:source) { :merge_request }
+ let(:source) { :merge_request_event }
let(:merge_request) do
create(:merge_request,
diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
index 553fc0fb9bf..22ca681cfd3 100644
--- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
@@ -68,6 +68,20 @@ describe Gitlab::Ci::Build::Policy::Refs do
expect(described_class.new(%w[triggers]))
.not_to be_satisfied_by(pipeline)
end
+
+ context 'when source is merge_request_event' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, source: :merge_request_event) }
+
+ it 'is satisfied with only: merge_request' do
+ expect(described_class.new(%w[merge_requests]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is not satisfied with only: merge_request_event' do
+ expect(described_class.new(%w[merge_request_events]))
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
end
context 'when matching a ref by a regular expression' do
@@ -78,10 +92,49 @@ describe Gitlab::Ci::Build::Policy::Refs do
.to be_satisfied_by(pipeline)
end
+ it 'is satisfied when case-insensitive regexp matches pipeline ref' do
+ expect(described_class.new(['/DOCS-.*/i']))
+ .to be_satisfied_by(pipeline)
+ end
+
it 'is not satisfied when regexp does not match pipeline ref' do
expect(described_class.new(['/fix-.*/']))
.not_to be_satisfied_by(pipeline)
end
+
+ context 'when unsafe regexp is used' do
+ let(:subject) { described_class.new(['/^(?!master).+/']) }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: false)
+ end
+
+ it 'ignores invalid regexp' do
+ expect(subject)
+ .not_to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when allow_unsafe_ruby_regexp is enabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: true)
+ end
+
+ it 'is satisfied by regexp' do
+ expect(subject)
+ .to be_satisfied_by(pipeline)
+ end
+ end
+ end
+ end
+
+ context 'malicious regexp' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, ref: malicious_text) }
+
+ subject { described_class.new([malicious_regexp_ruby]) }
+
+ include_examples 'malicious regexp'
end
end
end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
index c2c0742efc3..9b016901a20 100644
--- a/spec/lib/gitlab/ci/build/policy/variables_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -15,6 +15,7 @@ describe Gitlab::Ci::Build::Policy::Variables do
before do
pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '')
+ pipeline.variables.build(key: 'MY_VARIABLE', value: 'my-var')
end
describe '#satisfied_by?' do
@@ -24,6 +25,12 @@ describe Gitlab::Ci::Build::Policy::Variables do
expect(policy).to be_satisfied_by(pipeline, seed)
end
+ it 'is satisfied by a matching pipeline variable' do
+ policy = described_class.new(['$MY_VARIABLE'])
+
+ expect(policy).to be_satisfied_by(pipeline, seed)
+ end
+
it 'is not satisfied by an overridden empty variable' do
policy = described_class.new(['$CI_PROJECT_NAME'])
@@ -68,5 +75,19 @@ describe Gitlab::Ci::Build::Policy::Variables do
expect(pipeline).not_to be_persisted
expect(seed.to_resource).not_to be_persisted
end
+
+ context 'when a bridge job is used' do
+ let(:bridge) do
+ build(:ci_bridge, pipeline: pipeline, project: project, ref: 'master')
+ end
+
+ let(:seed) { double('bridge seed', to_resource: bridge) }
+
+ it 'is satisfied by a matching expression for a bridge job' do
+ policy = described_class.new(['$MY_VARIABLE'])
+
+ expect(policy).to be_satisfied_by(pipeline, seed)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/build/port_spec.rb b/spec/lib/gitlab/ci/build/port_spec.rb
new file mode 100644
index 00000000000..1413780dfa6
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/port_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Port do
+ subject { described_class.new(port) }
+
+ context 'when port is defined as an integer' do
+ let(:port) { 80 }
+
+ it 'populates the object' do
+ expect(subject.number).to eq 80
+ expect(subject.protocol).to eq described_class::DEFAULT_PORT_PROTOCOL
+ expect(subject.name).to eq described_class::DEFAULT_PORT_NAME
+ end
+ end
+
+ context 'when port is defined as hash' do
+ let(:port) { { number: 80, protocol: 'https', name: 'port_name' } }
+
+ it 'populates the object' do
+ expect(subject.number).to eq 80
+ expect(subject.protocol).to eq 'https'
+ expect(subject.name).to eq 'port_name'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb
new file mode 100644
index 00000000000..5187f99a441
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/prerequisite/factory_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Prerequisite::Factory do
+ let(:build) { create(:ci_build) }
+
+ describe '.for_build' do
+ let(:kubernetes_namespace) do
+ instance_double(
+ Gitlab::Ci::Build::Prerequisite::KubernetesNamespace,
+ unmet?: unmet)
+ end
+
+ subject { described_class.new(build).unmet }
+
+ before do
+ expect(Gitlab::Ci::Build::Prerequisite::KubernetesNamespace)
+ .to receive(:new).with(build).and_return(kubernetes_namespace)
+ end
+
+ context 'prerequisite is unmet' do
+ let(:unmet) { true }
+
+ it { is_expected.to eq [kubernetes_namespace] }
+ end
+
+ context 'prerequisite is met' do
+ let(:unmet) { false }
+
+ it { is_expected.to be_empty }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
new file mode 100644
index 00000000000..5387863bd07
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/prerequisite/kubernetes_namespace_spec.rb
@@ -0,0 +1,97 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Prerequisite::KubernetesNamespace do
+ let(:build) { create(:ci_build) }
+
+ describe '#unmet?' do
+ subject { described_class.new(build).unmet? }
+
+ context 'build has no deployment' do
+ before do
+ expect(build.deployment).to be_nil
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'build has a deployment' do
+ let!(:deployment) { create(:deployment, deployable: build) }
+
+ context 'and a cluster to deploy to' do
+ let(:cluster) { create(:cluster, :group) }
+
+ before do
+ allow(build.deployment).to receive(:cluster).and_return(cluster)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'and the cluster is not managed' do
+ let(:cluster) { create(:cluster, :not_managed, projects: [build.project]) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and a namespace is already created for this project' do
+ let!(:kubernetes_namespace) { create(:cluster_kubernetes_namespace, cluster: cluster, project: build.project) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and cluster is project type' do
+ let(:cluster) { create(:cluster, :project) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'and no cluster to deploy to' do
+ before do
+ expect(deployment.cluster).to be_nil
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe '#complete!' do
+ let!(:deployment) { create(:deployment, deployable: build) }
+ let(:service) { double(execute: true) }
+
+ subject { described_class.new(build).complete! }
+
+ context 'completion is required' do
+ let(:cluster) { create(:cluster, :group) }
+
+ before do
+ allow(build.deployment).to receive(:cluster).and_return(cluster)
+ end
+
+ it 'creates a kubernetes namespace' do
+ expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService)
+ .to receive(:new)
+ .with(cluster: cluster, kubernetes_namespace: instance_of(Clusters::KubernetesNamespace))
+ .and_return(service)
+
+ expect(service).to receive(:execute).once
+
+ subject
+ end
+ end
+
+ context 'completion is not required' do
+ before do
+ expect(deployment.cluster).to be_nil
+ end
+
+ it 'does not create a namespace' do
+ expect(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:new)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/environment_spec.rb b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
index 3c0007f4d57..0bc9e8bd3cd 100644
--- a/spec/lib/gitlab/ci/config/entry/environment_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/environment_spec.rb
@@ -100,6 +100,26 @@ describe Gitlab::Ci::Config::Entry::Environment do
end
end
+ context 'when wrong action type is used' do
+ let(:config) do
+ { name: 'production',
+ action: ['stop'] }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about wrong action type' do
+ expect(entry.errors)
+ .to include 'environment action should be a string'
+ end
+ end
+ end
+
context 'when invalid action is used' do
let(:config) do
{ name: 'production',
@@ -151,6 +171,26 @@ describe Gitlab::Ci::Config::Entry::Environment do
end
end
+ context 'when wrong url type is used' do
+ let(:config) do
+ { name: 'production',
+ url: ['https://meow.meow'] }
+ end
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ describe '#errors' do
+ it 'contains error about wrong url type' do
+ expect(entry.errors)
+ .to include 'environment url should be a string'
+ end
+ end
+ end
+
context 'when variables are used for environment' do
let(:config) do
{ name: 'review/$CI_COMMIT_REF_NAME',
diff --git a/spec/lib/gitlab/ci/config/entry/global_spec.rb b/spec/lib/gitlab/ci/config/entry/global_spec.rb
index 7651f594a4c..e23efff18d5 100644
--- a/spec/lib/gitlab/ci/config/entry/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/global_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::Ci::Config::Entry::Global do
expect(described_class.nodes.keys)
.to match_array(%i[before_script image services
after_script variables stages
- types cache])
+ types cache include])
end
end
end
@@ -42,7 +42,7 @@ describe Gitlab::Ci::Config::Entry::Global do
end
it 'creates node object for each entry' do
- expect(global.descendants.count).to eq 8
+ expect(global.descendants.count).to eq 9
end
it 'creates node object using valid class' do
@@ -189,7 +189,7 @@ describe Gitlab::Ci::Config::Entry::Global do
describe '#nodes' do
it 'instantizes all nodes' do
- expect(global.descendants.count).to eq 8
+ expect(global.descendants.count).to eq 9
end
it 'contains unspecified nodes' do
diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb
index 1a4d9ed5517..1ebdda398b9 100644
--- a/spec/lib/gitlab/ci/config/entry/image_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb
@@ -35,6 +35,12 @@ describe Gitlab::Ci::Config::Entry::Image do
expect(entry.entrypoint).to be_nil
end
end
+
+ describe '#ports' do
+ it "returns image's ports" do
+ expect(entry.ports).to be_nil
+ end
+ end
end
context 'when configuration is a hash' do
@@ -69,6 +75,38 @@ describe Gitlab::Ci::Config::Entry::Image do
expect(entry.entrypoint).to eq %w(/bin/sh run)
end
end
+
+ context 'when configuration has ports' do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+ let(:config) { { name: 'ruby:2.2', entrypoint: %w(/bin/sh run), ports: ports } }
+ let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:image_ports) { false }
+
+ context 'when with_image_ports metadata is not enabled' do
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("image config contains disallowed keys: ports")
+ end
+ end
+ end
+
+ context 'when with_image_ports metadata is enabled' do
+ let(:image_ports) { true }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#ports' do
+ it "returns image's ports" do
+ expect(entry.ports).to eq ports
+ end
+ end
+ end
+ end
end
context 'when entry value is not correct' do
@@ -76,8 +114,8 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'image config should be a hash or a string'
+ expect(entry.errors.first)
+ .to match /config should be a hash or a string/
end
end
@@ -93,8 +131,8 @@ describe Gitlab::Ci::Config::Entry::Image do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'image config contains unknown keys: non_existing'
+ expect(entry.errors.first)
+ .to match /config contains unknown keys: non_existing/
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb
index 0560eb42e4d..e0552ae8c57 100644
--- a/spec/lib/gitlab/ci/config/entry/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb
@@ -94,7 +94,7 @@ describe Gitlab::Ci::Config::Entry::Job do
it 'returns error about wrong value type' do
expect(entry).not_to be_valid
- expect(entry.errors).to include "job extends should be a string"
+ expect(entry.errors).to include "job extends should be an array of strings or a string"
end
end
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 1c987e13a9a..fba5671594d 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -1,4 +1,5 @@
require 'fast_spec_helper'
+require 'support/helpers/stub_feature_flags'
require_dependency 'active_model'
describe Gitlab::Ci::Config::Entry::Policy do
@@ -33,6 +34,44 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
+ context 'when config is an empty regexp' do
+ let(:config) { ['//'] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when using unsafe regexp' do
+ include StubFeatureFlags
+
+ let(:config) { ['/^(?!master).+/'] }
+
+ subject { described_class.new([regexp]) }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: false)
+ end
+
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ context 'when allow_unsafe_ruby_regexp is enabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: true)
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
context 'when config is a special keyword' do
let(:config) { %w[tags triggers branches] }
@@ -67,6 +106,34 @@ describe Gitlab::Ci::Config::Entry::Policy do
end
end
+ context 'when using unsafe regexp' do
+ include StubFeatureFlags
+
+ let(:config) { { refs: ['/^(?!master).+/'] } }
+
+ subject { described_class.new([regexp]) }
+
+ context 'when allow_unsafe_ruby_regexp is disabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: false)
+ end
+
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ context 'when allow_unsafe_ruby_regexp is enabled' do
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: true)
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
context 'when specifying kubernetes policy' do
let(:config) { { kubernetes: 'active' } }
diff --git a/spec/lib/gitlab/ci/config/entry/port_spec.rb b/spec/lib/gitlab/ci/config/entry/port_spec.rb
new file mode 100644
index 00000000000..5f8f294334e
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/port_spec.rb
@@ -0,0 +1,173 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Port do
+ let(:entry) { described_class.new(config) }
+
+ before do
+ entry.compose!
+ end
+
+ context 'when configuration is a string' do
+ let(:config) { 80 }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq(number: 80)
+ end
+ end
+
+ describe '#number' do
+ it "returns port number" do
+ expect(entry.number).to eq 80
+ end
+ end
+
+ describe '#protocol' do
+ it "is nil" do
+ expect(entry.protocol).to be_nil
+ end
+ end
+
+ describe '#name' do
+ it "is nil" do
+ expect(entry.name).to be_nil
+ end
+ end
+ end
+
+ context 'when configuration is a hash' do
+ context 'with the complete hash' do
+ let(:config) do
+ { number: 80,
+ protocol: 'http',
+ name: 'foobar' }
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq config
+ end
+ end
+
+ describe '#number' do
+ it "returns port number" do
+ expect(entry.number).to eq 80
+ end
+ end
+
+ describe '#protocol' do
+ it "returns port protocol" do
+ expect(entry.protocol).to eq 'http'
+ end
+ end
+
+ describe '#name' do
+ it "returns port name" do
+ expect(entry.name).to eq 'foobar'
+ end
+ end
+ end
+
+ context 'with only the port number' do
+ let(:config) { { number: 80 } }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid hash' do
+ expect(entry.value).to eq(number: 80)
+ end
+ end
+
+ describe '#number' do
+ it "returns port number" do
+ expect(entry.number).to eq 80
+ end
+ end
+
+ describe '#protocol' do
+ it "is nil" do
+ expect(entry.protocol).to be_nil
+ end
+ end
+
+ describe '#name' do
+ it "is nil" do
+ expect(entry.name).to be_nil
+ end
+ end
+ end
+
+ context 'without the number' do
+ let(:config) { { protocol: 'http' } }
+
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+ end
+
+ context 'when configuration is invalid' do
+ let(:config) { '80' }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).not_to be_valid
+ end
+ end
+ end
+
+ context 'when protocol' do
+ let(:config) { { number: 80, protocol: protocol, name: 'foobar' } }
+
+ context 'is http' do
+ let(:protocol) { 'http' }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'is https' do
+ let(:protocol) { 'https' }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'is neither http nor https' do
+ let(:protocol) { 'foo' }
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry.errors).to include("port protocol should be http or https")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/ports_spec.rb b/spec/lib/gitlab/ci/config/entry/ports_spec.rb
new file mode 100644
index 00000000000..2063bd1d86c
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/entry/ports_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Entry::Ports do
+ let(:entry) { described_class.new(config) }
+
+ before do
+ entry.compose!
+ end
+
+ context 'when configuration is valid' do
+ let(:config) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid array' do
+ expect(entry.value).to eq(config)
+ end
+ end
+ end
+
+ context 'when configuration is invalid' do
+ let(:config) { 'postgresql:9.5' }
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ end
+ end
+
+ context 'when any of the ports' do
+ before do
+ expect(entry).not_to be_valid
+ expect(entry.errors.count).to eq 1
+ end
+
+ context 'have the same name' do
+ let(:config) do
+ [{ number: 80, protocol: 'http', name: 'foobar' },
+ { number: 81, protocol: 'http', name: 'foobar' }]
+ end
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry.errors.first).to match /each port name must be different/
+ end
+ end
+ end
+
+ context 'have the same port' do
+ let(:config) do
+ [{ number: 80, protocol: 'http', name: 'foobar' },
+ { number: 80, protocol: 'http', name: 'foobar1' }]
+ end
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry.errors.first).to match /each port number can only be referenced once/
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb
index 9ebf947a751..d31866a1987 100644
--- a/spec/lib/gitlab/ci/config/entry/service_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb
@@ -39,6 +39,12 @@ describe Gitlab::Ci::Config::Entry::Service do
expect(entry.command).to be_nil
end
end
+
+ describe '#ports' do
+ it "returns service's ports" do
+ expect(entry.ports).to be_nil
+ end
+ end
end
context 'when configuration is a hash' do
@@ -81,6 +87,50 @@ describe Gitlab::Ci::Config::Entry::Service do
expect(entry.entrypoint).to eq %w(/bin/sh run)
end
end
+
+ context 'when configuration has ports' do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+ let(:config) do
+ { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
+ end
+ let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:image_ports) { false }
+
+ context 'when with_image_ports metadata is not enabled' do
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("service config contains disallowed keys: ports")
+ end
+ end
+ end
+
+ context 'when with_image_ports metadata is enabled' do
+ let(:image_ports) { true }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+
+ context 'when unknown port keys detected' do
+ let(:ports) { [{ number: 80, invalid_key: 'foo' }] }
+
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors.first)
+ .to match /port config contains unknown keys: invalid_key/
+ end
+ end
+ end
+
+ describe '#ports' do
+ it "returns image's ports" do
+ expect(entry.ports).to eq ports
+ end
+ end
+ end
+ end
end
context 'when entry value is not correct' do
@@ -88,8 +138,8 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'service config should be a hash or a string'
+ expect(entry.errors.first)
+ .to match /config should be a hash or a string/
end
end
@@ -105,8 +155,8 @@ describe Gitlab::Ci::Config::Entry::Service do
describe '#errors' do
it 'saves errors' do
- expect(entry.errors)
- .to include 'service config contains unknown keys: non_existing'
+ expect(entry.errors.first)
+ .to match /config contains unknown keys: non_existing/
end
end
@@ -116,4 +166,26 @@ describe Gitlab::Ci::Config::Entry::Service do
end
end
end
+
+ context 'when service has ports' do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+ let(:config) do
+ { name: 'postgresql:9.5', command: %w(cmd run), entrypoint: %w(/bin/sh run), ports: ports }
+ end
+
+ it 'alias field is mandatory' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("service alias can't be blank")
+ end
+ end
+
+ context 'when service does not have ports' do
+ let(:config) do
+ { name: 'postgresql:9.5', alias: 'db', command: %w(cmd run), entrypoint: %w(/bin/sh run) }
+ end
+
+ it 'alias field is optional' do
+ expect(entry).to be_valid
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/entry/services_spec.rb b/spec/lib/gitlab/ci/config/entry/services_spec.rb
index 7c4319aee63..d5a1316f665 100644
--- a/spec/lib/gitlab/ci/config/entry/services_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/services_spec.rb
@@ -32,4 +32,91 @@ describe Gitlab::Ci::Config::Entry::Services do
end
end
end
+
+ context 'when configuration has ports' do
+ let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] }
+ let(:config) { ['postgresql:9.5', { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }] }
+ let(:entry) { described_class.new(config, { with_image_ports: image_ports }) }
+ let(:image_ports) { false }
+
+ context 'when with_image_ports metadata is not enabled' do
+ describe '#valid?' do
+ it 'is not valid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("service config contains disallowed keys: ports")
+ end
+ end
+ end
+
+ context 'when with_image_ports metadata is enabled' do
+ let(:image_ports) { true }
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ describe '#value' do
+ it 'returns valid array' do
+ expect(entry.value).to eq([{ name: 'postgresql:9.5' }, { name: 'postgresql:9.1', alias: 'postgres_old', ports: ports }])
+ end
+ end
+
+ describe 'services alias' do
+ context 'when they are not unique' do
+ let(:config) do
+ ['postgresql:9.5',
+ { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
+ { name: 'ruby', alias: 'postgres_old', ports: [81] }]
+ end
+
+ describe '#valid?' do
+ it 'is invalid' do
+ expect(entry).not_to be_valid
+ expect(entry.errors).to include("services config alias must be unique in services with ports")
+ end
+ end
+ end
+
+ context 'when they are unique' do
+ let(:config) do
+ ['postgresql:9.5',
+ { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
+ { name: 'ruby', alias: 'ruby', ports: [81] }]
+ end
+
+ describe '#valid?' do
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+
+ context 'when one of the duplicated alias is in a service without ports' do
+ let(:config) do
+ ['postgresql:9.5',
+ { name: 'postgresql:9.1', alias: 'postgres_old', ports: [80] },
+ { name: 'ruby', alias: 'postgres_old' }]
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+
+ context 'when there are not any ports' do
+ let(:config) do
+ ['postgresql:9.5',
+ { name: 'postgresql:9.1', alias: 'postgres_old' },
+ { name: 'ruby', alias: 'postgres_old' }]
+ end
+
+ it 'is valid' do
+ expect(entry).to be_valid
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
index 0a148375d11..d63612053b6 100644
--- a/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
+++ b/spec/lib/gitlab/ci/config/extendable/entry_spec.rb
@@ -44,12 +44,12 @@ describe Gitlab::Ci::Config::Extendable::Entry do
end
end
- describe '#extends_key' do
+ describe '#extends_keys' do
context 'when entry is extensible' do
it 'returns symbolized extends key value' do
entry = described_class.new(:test, test: { extends: 'something' })
- expect(entry.extends_key).to eq :something
+ expect(entry.extends_keys).to eq [:something]
end
end
@@ -57,7 +57,7 @@ describe Gitlab::Ci::Config::Extendable::Entry do
it 'returns nil' do
entry = described_class.new(:test, test: 'something')
- expect(entry.extends_key).to be_nil
+ expect(entry.extends_keys).to be_nil
end
end
end
@@ -76,7 +76,7 @@ describe Gitlab::Ci::Config::Extendable::Entry do
end
end
- describe '#base_hash!' do
+ describe '#base_hashes!' do
subject { described_class.new(:test, hash) }
context 'when base hash is not extensible' do
@@ -87,8 +87,8 @@ describe Gitlab::Ci::Config::Extendable::Entry do
}
end
- it 'returns unchanged base hash' do
- expect(subject.base_hash!).to eq(script: 'rspec')
+ it 'returns unchanged base hashes' do
+ expect(subject.base_hashes!).to eq([{ script: 'rspec' }])
end
end
@@ -101,12 +101,12 @@ describe Gitlab::Ci::Config::Extendable::Entry do
}
end
- it 'extends the base hash first' do
- expect(subject.base_hash!).to eq(extends: 'first', script: 'rspec')
+ it 'extends the base hashes first' do
+ expect(subject.base_hashes!).to eq([{ extends: 'first', script: 'rspec' }])
end
it 'mutates original context' do
- subject.base_hash!
+ subject.base_hashes!
expect(hash.fetch(:second)).to eq(extends: 'first', script: 'rspec')
end
@@ -171,6 +171,34 @@ describe Gitlab::Ci::Config::Extendable::Entry do
end
end
+ context 'when extending multiple hashes correctly' do
+ let(:hash) do
+ {
+ first: { script: 'my value', image: 'ubuntu' },
+ second: { image: 'alpine' },
+ test: { extends: %w(first second) }
+ }
+ end
+
+ let(:result) do
+ {
+ first: { script: 'my value', image: 'ubuntu' },
+ second: { image: 'alpine' },
+ test: { extends: %w(first second), script: 'my value', image: 'alpine' }
+ }
+ end
+
+ it 'returns extended part of the hash' do
+ expect(subject.extend!).to eq result[:test]
+ end
+
+ it 'mutates original context' do
+ subject.extend!
+
+ expect(hash).to eq result
+ end
+ end
+
context 'when hash is not extensible' do
let(:hash) do
{
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index 1a6b3587599..dd536a241bd 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -3,7 +3,7 @@
require 'fast_spec_helper'
describe Gitlab::Ci::Config::External::File::Base do
- let(:context) { described_class::Context.new(nil, 'HEAD', nil) }
+ let(:context) { described_class::Context.new(nil, 'HEAD', nil, Set.new) }
let(:test_class) do
Class.new(described_class) do
@@ -26,7 +26,7 @@ describe Gitlab::Ci::Config::External::File::Base do
context 'when a location is present' do
let(:location) { 'some-location' }
- it 'should return true' do
+ it 'returns true' do
expect(subject).to be_matching
end
end
@@ -34,7 +34,7 @@ describe Gitlab::Ci::Config::External::File::Base do
context 'with a location is missing' do
let(:location) { nil }
- it 'should return false' do
+ it 'returns false' do
expect(subject).not_to be_matching
end
end
@@ -79,4 +79,20 @@ describe Gitlab::Ci::Config::External::File::Base do
end
end
end
+
+ describe '#to_hash' do
+ context 'with includes' do
+ let(:location) { 'some/file/config.yml' }
+ let(:content) { 'include: { template: Bash.gitlab-ci.yml }'}
+
+ before do
+ allow_any_instance_of(test_class)
+ .to receive(:content).and_return(content)
+ end
+
+ it 'does expand hash to include the template' do
+ expect(subject.to_hash).to include(:before_script)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
index ff67a765da0..9451db9522a 100644
--- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb
@@ -4,8 +4,10 @@ require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Local do
set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
- let(:context) { described_class::Context.new(project, '12345', nil) }
+ let(:sha) { '12345' }
+ let(:context) { described_class::Context.new(project, sha, user, Set.new) }
let(:params) { { local: location } }
let(:local_file) { described_class.new(params, context) }
@@ -13,7 +15,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'when a local is specified' do
let(:params) { { local: 'file' } }
- it 'should return true' do
+ it 'returns true' do
expect(local_file).to be_matching
end
end
@@ -21,7 +23,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'with a missing local' do
let(:params) { { local: nil } }
- it 'should return false' do
+ it 'returns false' do
expect(local_file).not_to be_matching
end
end
@@ -29,7 +31,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'with a missing local key' do
let(:params) { {} }
- it 'should return false' do
+ it 'returns false' do
expect(local_file).not_to be_matching
end
end
@@ -43,7 +45,7 @@ describe Gitlab::Ci::Config::External::File::Local do
allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("image: 'ruby2:2'")
end
- it 'should return true' do
+ it 'returns true' do
expect(local_file.valid?).to be_truthy
end
end
@@ -51,7 +53,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'when is not a valid local path' do
let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
- it 'should return false' do
+ it 'returns false' do
expect(local_file.valid?).to be_falsy
end
end
@@ -59,7 +61,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'when is not a yaml file' do
let(:location) { '/config/application.rb' }
- it 'should return false' do
+ it 'returns false' do
expect(local_file.valid?).to be_falsy
end
end
@@ -82,7 +84,7 @@ describe Gitlab::Ci::Config::External::File::Local do
allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return(local_file_content)
end
- it 'should return the content of the file' do
+ it 'returns the content of the file' do
expect(local_file.content).to eq(local_file_content)
end
end
@@ -90,7 +92,7 @@ describe Gitlab::Ci::Config::External::File::Local do
context 'with an invalid file' do
let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
- it 'should be nil' do
+ it 'is nil' do
expect(local_file.content).to be_nil
end
end
@@ -99,8 +101,40 @@ describe Gitlab::Ci::Config::External::File::Local do
describe '#error_message' do
let(:location) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
- it 'should return an error message' do
+ it 'returns an error message' do
expect(local_file.error_message).to eq("Local file `#{location}` does not exist!")
end
end
+
+ describe '#expand_context' do
+ let(:location) { 'location.yml' }
+
+ subject { local_file.send(:expand_context) }
+
+ it 'inherits project, user and sha' do
+ is_expected.to include(user: user, project: project, sha: sha)
+ end
+ end
+
+ describe '#to_hash' do
+ context 'properly includes another local file in the same repository' do
+ let(:location) { 'some/file/config.yml' }
+ let(:content) { 'include: { local: another-config.yml }'}
+
+ let(:another_location) { 'another-config.yml' }
+ let(:another_content) { 'rspec: JOB' }
+
+ before do
+ allow(project.repository).to receive(:blob_data_at).with(sha, location)
+ .and_return(content)
+
+ allow(project.repository).to receive(:blob_data_at).with(sha, another_location)
+ .and_return(another_content)
+ end
+
+ it 'does expand hash to include the template' do
+ expect(local_file.to_hash).to include(:rspec)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
index 11809adcaf6..4acb4f324d3 100644
--- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb
@@ -3,12 +3,13 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Project do
+ set(:context_project) { create(:project) }
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:context_user) { user }
- let(:context) { described_class::Context.new(nil, '12345', context_user) }
- let(:subject) { described_class.new(params, context) }
+ let(:context) { described_class::Context.new(context_project, '12345', context_user, Set.new) }
+ let(:project_file) { described_class.new(params, context) }
before do
project.add_developer(user)
@@ -18,32 +19,32 @@ describe Gitlab::Ci::Config::External::File::Project do
context 'when a file and project is specified' do
let(:params) { { file: 'file.yml', project: 'project' } }
- it 'should return true' do
- expect(subject).to be_matching
+ it 'returns true' do
+ expect(project_file).to be_matching
end
end
context 'with only file is specified' do
let(:params) { { file: 'file.yml' } }
- it 'should return false' do
- expect(subject).not_to be_matching
+ it 'returns false' do
+ expect(project_file).not_to be_matching
end
end
context 'with only project is specified' do
let(:params) { { project: 'project' } }
- it 'should return false' do
- expect(subject).not_to be_matching
+ it 'returns false' do
+ expect(project_file).not_to be_matching
end
end
context 'with a missing local key' do
let(:params) { {} }
- it 'should return false' do
- expect(subject).not_to be_matching
+ it 'returns false' do
+ expect(project_file).not_to be_matching
end
end
end
@@ -60,16 +61,16 @@ describe Gitlab::Ci::Config::External::File::Project do
stub_project_blob(root_ref_sha, '/file.yml') { 'image: ruby:2.1' }
end
- it 'should return true' do
- expect(subject).to be_valid
+ it 'returns true' do
+ expect(project_file).to be_valid
end
context 'when user does not have permission to access file' do
let(:context_user) { create(:user) }
- it 'should return false' do
- expect(subject).not_to be_valid
- expect(subject.error_message).to include("Project `#{project.full_path}` not found or access denied!")
+ it 'returns false' do
+ expect(project_file).not_to be_valid
+ expect(project_file.error_message).to include("Project `#{project.full_path}` not found or access denied!")
end
end
end
@@ -85,8 +86,8 @@ describe Gitlab::Ci::Config::External::File::Project do
stub_project_blob(ref_sha, '/file.yml') { 'image: ruby:2.1' }
end
- it 'should return true' do
- expect(subject).to be_valid
+ it 'returns true' do
+ expect(project_file).to be_valid
end
end
@@ -101,9 +102,9 @@ describe Gitlab::Ci::Config::External::File::Project do
stub_project_blob(root_ref_sha, '/file.yml') { '' }
end
- it 'should return false' do
- expect(subject).not_to be_valid
- expect(subject.error_message).to include("Project `#{project.full_path}` file `/file.yml` is empty!")
+ it 'returns false' do
+ expect(project_file).not_to be_valid
+ expect(project_file.error_message).to include("Project `#{project.full_path}` file `/file.yml` is empty!")
end
end
@@ -112,9 +113,9 @@ describe Gitlab::Ci::Config::External::File::Project do
{ project: project.full_path, ref: 'I-Do-Not-Exist', file: '/file.yml' }
end
- it 'should return false' do
- expect(subject).not_to be_valid
- expect(subject.error_message).to include("Project `#{project.full_path}` reference `I-Do-Not-Exist` does not exist!")
+ it 'returns false' do
+ expect(project_file).not_to be_valid
+ expect(project_file.error_message).to include("Project `#{project.full_path}` reference `I-Do-Not-Exist` does not exist!")
end
end
@@ -123,9 +124,9 @@ describe Gitlab::Ci::Config::External::File::Project do
{ project: project.full_path, file: '/invalid-file.yml' }
end
- it 'should return false' do
- expect(subject).not_to be_valid
- expect(subject.error_message).to include("Project `#{project.full_path}` file `/invalid-file.yml` does not exist!")
+ it 'returns false' do
+ expect(project_file).not_to be_valid
+ expect(project_file.error_message).to include("Project `#{project.full_path}` file `/invalid-file.yml` does not exist!")
end
end
@@ -134,13 +135,23 @@ describe Gitlab::Ci::Config::External::File::Project do
{ project: project.full_path, file: '/invalid-file' }
end
- it 'should return false' do
- expect(subject).not_to be_valid
- expect(subject.error_message).to include('Included file `/invalid-file` does not have YAML extension!')
+ it 'returns false' do
+ expect(project_file).not_to be_valid
+ expect(project_file.error_message).to include('Included file `/invalid-file` does not have YAML extension!')
end
end
end
+ describe '#expand_context' do
+ let(:params) { { file: 'file.yml', project: project.full_path, ref: 'master' } }
+
+ subject { project_file.send(:expand_context) }
+
+ it 'inherits user, and target project and sha' do
+ is_expected.to include(user: user, project: project, sha: project.commit('master').id)
+ end
+ end
+
private
def stub_project_blob(ref, path)
diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
index 3e0fda9c308..46d68097fff 100644
--- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Remote do
- let(:context) { described_class::Context.new(nil, '12345', nil) }
+ include StubRequests
+
+ let(:context) { described_class::Context.new(nil, '12345', nil, Set.new) }
let(:params) { { remote: location } }
let(:remote_file) { described_class.new(params, context) }
let(:location) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
@@ -21,7 +23,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when a remote is specified' do
let(:params) { { remote: 'http://remote' } }
- it 'should return true' do
+ it 'returns true' do
expect(remote_file).to be_matching
end
end
@@ -29,7 +31,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with a missing remote' do
let(:params) { { remote: nil } }
- it 'should return false' do
+ it 'returns false' do
expect(remote_file).not_to be_matching
end
end
@@ -37,7 +39,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with a missing remote key' do
let(:params) { {} }
- it 'should return false' do
+ it 'returns false' do
expect(remote_file).not_to be_matching
end
end
@@ -46,10 +48,10 @@ describe Gitlab::Ci::Config::External::File::Remote do
describe "#valid?" do
context 'when is a valid remote url' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content)
+ stub_full_request(location).to_return(body: remote_file_content)
end
- it 'should return true' do
+ it 'returns true' do
expect(remote_file.valid?).to be_truthy
end
end
@@ -57,7 +59,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with an irregular url' do
let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
- it 'should return false' do
+ it 'returns false' do
expect(remote_file.valid?).to be_falsy
end
end
@@ -67,7 +69,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error)
end
- it 'should be falsy' do
+ it 'is falsy' do
expect(remote_file.valid?).to be_falsy
end
end
@@ -75,7 +77,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when is not a yaml file' do
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
- it 'should be falsy' do
+ it 'is falsy' do
expect(remote_file.valid?).to be_falsy
end
end
@@ -83,7 +85,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with an internal url' do
let(:location) { 'http://localhost:8080' }
- it 'should be falsy' do
+ it 'is falsy' do
expect(remote_file.valid?).to be_falsy
end
end
@@ -92,10 +94,10 @@ describe Gitlab::Ci::Config::External::File::Remote do
describe "#content" do
context 'with a valid remote file' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content)
+ stub_full_request(location).to_return(body: remote_file_content)
end
- it 'should return the content of the file' do
+ it 'returns the content of the file' do
expect(remote_file.content).to eql(remote_file_content)
end
end
@@ -105,7 +107,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error)
end
- it 'should be falsy' do
+ it 'is falsy' do
expect(remote_file.content).to be_falsy
end
end
@@ -114,10 +116,10 @@ describe Gitlab::Ci::Config::External::File::Remote do
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
before do
- WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error'))
+ stub_full_request(location).to_raise(SocketError.new('Some HTTP error'))
end
- it 'should be nil' do
+ it 'is nil' do
expect(remote_file.content).to be_nil
end
end
@@ -125,7 +127,7 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'with an internal url' do
let(:location) { 'http://localhost:8080' }
- it 'should be nil' do
+ it 'is nil' do
expect(remote_file.content).to be_nil
end
end
@@ -144,30 +146,30 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when timeout error has been raised' do
before do
- WebMock.stub_request(:get, location).to_timeout
+ stub_full_request(location).to_timeout
end
- it 'should returns error message about a timeout' do
+ it 'returns error message about a timeout' do
expect(subject).to match /could not be fetched because of a timeout error!/
end
end
context 'when HTTP error has been raised' do
before do
- WebMock.stub_request(:get, location).to_raise(Gitlab::HTTP::Error)
+ stub_full_request(location).to_raise(Gitlab::HTTP::Error)
end
- it 'should returns error message about a HTTP error' do
+ it 'returns error message about a HTTP error' do
expect(subject).to match /could not be fetched because of HTTP error!/
end
end
context 'when response has 404 status' do
before do
- WebMock.stub_request(:get, location).to_return(body: remote_file_content, status: 404)
+ stub_full_request(location).to_return(body: remote_file_content, status: 404)
end
- it 'should returns error message about a timeout' do
+ it 'returns error message about a timeout' do
expect(subject).to match /could not be fetched because of HTTP code `404` error!/
end
end
@@ -175,10 +177,20 @@ describe Gitlab::Ci::Config::External::File::Remote do
context 'when the URL is blocked' do
let(:location) { 'http://127.0.0.1/some/path/to/config.yaml' }
- it 'should include details about blocked URL' do
+ it 'includes details about blocked URL' do
expect(subject).to eq "Remote file could not be fetched because URL '#{location}' " \
'is blocked: Requests to localhost are not allowed!'
end
end
end
+
+ describe '#expand_context' do
+ let(:params) { { remote: 'http://remote' } }
+
+ subject { remote_file.send(:expand_context) }
+
+ it 'drops all parameters' do
+ is_expected.to include(user: nil, project: nil, sha: nil)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
index 1fb5655309a..1609b8fd66b 100644
--- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb
@@ -3,34 +3,37 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::File::Template do
- let(:context) { described_class::Context.new(nil, '12345') }
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ let(:context) { described_class::Context.new(project, '12345', user, Set.new) }
let(:template) { 'Auto-DevOps.gitlab-ci.yml' }
let(:params) { { template: template } }
- subject { described_class.new(params, context) }
+ let(:template_file) { described_class.new(params, context) }
describe '#matching?' do
context 'when a template is specified' do
let(:params) { { template: 'some-template' } }
- it 'should return true' do
- expect(subject).to be_matching
+ it 'returns true' do
+ expect(template_file).to be_matching
end
end
context 'with a missing template' do
let(:params) { { template: nil } }
- it 'should return false' do
- expect(subject).not_to be_matching
+ it 'returns false' do
+ expect(template_file).not_to be_matching
end
end
context 'with a missing template key' do
let(:params) { {} }
- it 'should return false' do
- expect(subject).not_to be_matching
+ it 'returns false' do
+ expect(template_file).not_to be_matching
end
end
end
@@ -39,32 +42,32 @@ describe Gitlab::Ci::Config::External::File::Template do
context 'when is a valid template name' do
let(:template) { 'Auto-DevOps.gitlab-ci.yml' }
- it 'should return true' do
- expect(subject).to be_valid
+ it 'returns true' do
+ expect(template_file).to be_valid
end
end
context 'with invalid template name' do
let(:template) { 'Template.yml' }
- it 'should return false' do
- expect(subject).not_to be_valid
- expect(subject.error_message).to include('Template file `Template.yml` is not a valid location!')
+ it 'returns false' do
+ expect(template_file).not_to be_valid
+ expect(template_file.error_message).to include('Template file `Template.yml` is not a valid location!')
end
end
context 'with a non-existing template' do
let(:template) { 'I-Do-Not-Have-This-Template.gitlab-ci.yml' }
- it 'should return false' do
- expect(subject).not_to be_valid
- expect(subject.error_message).to include('Included file `I-Do-Not-Have-This-Template.gitlab-ci.yml` is empty or does not exist!')
+ it 'returns false' do
+ expect(template_file).not_to be_valid
+ expect(template_file.error_message).to include('Included file `I-Do-Not-Have-This-Template.gitlab-ci.yml` is empty or does not exist!')
end
end
end
describe '#template_name' do
- let(:template_name) { subject.send(:template_name) }
+ let(:template_name) { template_file.send(:template_name) }
context 'when template does end with .gitlab-ci.yml' do
let(:template) { 'my-template.gitlab-ci.yml' }
@@ -90,4 +93,14 @@ describe Gitlab::Ci::Config::External::File::Template do
end
end
end
+
+ describe '#expand_context' do
+ let(:location) { 'location.yml' }
+
+ subject { template_file.send(:expand_context) }
+
+ it 'drops all parameters' do
+ is_expected.to include(user: nil, project: nil, sha: nil)
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
index 4cab4961b0f..e068b786b02 100644
--- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb
@@ -3,12 +3,15 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::Mapper do
+ include StubRequests
+
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:local_file) { '/lib/gitlab/ci/templates/non-existent-file.yml' }
let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' }
+ let(:expandset) { Set.new }
let(:file_content) do
<<~HEREDOC
@@ -17,11 +20,11 @@ describe Gitlab::Ci::Config::External::Mapper do
end
before do
- WebMock.stub_request(:get, remote_url).to_return(body: file_content)
+ stub_full_request(remote_url).to_return(body: file_content)
end
describe '#process' do
- subject { described_class.new(values, project: project, sha: '123456', user: user).process }
+ subject { described_class.new(values, project: project, sha: '123456', user: user, expandset: expandset).process }
context "when single 'include' keyword is defined" do
context 'when the string is a local file' do
@@ -141,5 +144,37 @@ describe Gitlab::Ci::Config::External::Mapper do
expect(subject).to be_empty
end
end
+
+ context "when duplicate 'include' is defined" do
+ let(:values) do
+ { include: [
+ { 'local' => local_file },
+ { 'local' => local_file }
+ ],
+ image: 'ruby:2.2' }
+ end
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(described_class::DuplicateIncludesError)
+ end
+ end
+
+ context "when too many 'includes' are defined" do
+ let(:values) do
+ { include: [
+ { 'local' => local_file },
+ { 'remote' => remote_url }
+ ],
+ image: 'ruby:2.2' }
+ end
+
+ before do
+ stub_const("#{described_class}::MAX_INCLUDES", 1)
+ end
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(described_class::TooManyIncludesError)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb
index 1ac58139b25..856187371e1 100644
--- a/spec/lib/gitlab/ci/config/external/processor_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb
@@ -3,20 +3,27 @@
require 'spec_helper'
describe Gitlab::Ci::Config::External::Processor do
+ include StubRequests
+
set(:project) { create(:project, :repository) }
+ set(:another_project) { create(:project, :repository) }
set(:user) { create(:user) }
- let(:processor) { described_class.new(values, project: project, sha: '12345', user: user) }
+ let(:expandset) { Set.new }
+ let(:sha) { '12345' }
+ let(:processor) { described_class.new(values, project: project, sha: '12345', user: user, expandset: expandset) }
before do
project.add_developer(user)
end
describe "#perform" do
+ subject { processor.perform }
+
context 'when no external files defined' do
let(:values) { { image: 'ruby:2.2' } }
- it 'should return the same values' do
+ it 'returns the same values' do
expect(processor.perform).to eq(values)
end
end
@@ -24,7 +31,7 @@ describe Gitlab::Ci::Config::External::Processor do
context 'when an invalid local file is defined' do
let(:values) { { include: '/lib/gitlab/ci/templates/non-existent-file.yml', image: 'ruby:2.2' } }
- it 'should raise an error' do
+ it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
"Local file `/lib/gitlab/ci/templates/non-existent-file.yml` does not exist!"
@@ -37,10 +44,10 @@ describe Gitlab::Ci::Config::External::Processor do
let(:values) { { include: remote_file, image: 'ruby:2.2' } }
before do
- WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error'))
+ stub_full_request(remote_file).and_raise(SocketError.new('Some HTTP error'))
end
- it 'should raise an error' do
+ it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
"Remote file `#{remote_file}` could not be fetched because of a socket error!"
@@ -70,15 +77,15 @@ describe Gitlab::Ci::Config::External::Processor do
end
before do
- WebMock.stub_request(:get, remote_file).to_return(body: external_file_content)
+ stub_full_request(remote_file).to_return(body: external_file_content)
end
- it 'should append the file to the values' do
+ it 'appends the file to the values' do
output = processor.perform
expect(output.keys).to match_array([:image, :before_script, :rspec, :rubocop])
end
- it "should remove the 'include' keyword" do
+ it "removes the 'include' keyword" do
expect(processor.perform[:include]).to be_nil
end
end
@@ -100,12 +107,12 @@ describe Gitlab::Ci::Config::External::Processor do
.to receive(:fetch_local_content).and_return(local_file_content)
end
- it 'should append the file to the values' do
+ it 'appends the file to the values' do
output = processor.perform
expect(output.keys).to match_array([:image, :before_script])
end
- it "should remove the 'include' keyword" do
+ it "removes the 'include' keyword" do
expect(processor.perform[:include]).to be_nil
end
end
@@ -140,14 +147,14 @@ describe Gitlab::Ci::Config::External::Processor do
allow_any_instance_of(Gitlab::Ci::Config::External::File::Local)
.to receive(:fetch_local_content).and_return(local_file_content)
- WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
+ stub_full_request(remote_file).to_return(body: remote_file_content)
end
- it 'should append the files to the values' do
+ it 'appends the files to the values' do
expect(processor.perform.keys).to match_array([:image, :stages, :before_script, :rspec])
end
- it "should remove the 'include' keyword" do
+ it "removes the 'include' keyword" do
expect(processor.perform[:include]).to be_nil
end
end
@@ -162,7 +169,7 @@ describe Gitlab::Ci::Config::External::Processor do
.to receive(:fetch_local_content).and_return(local_file_content)
end
- it 'should raise an error' do
+ it 'raises an error' do
expect { processor.perform }.to raise_error(
described_class::IncludeError,
"Included file `/lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!"
@@ -185,10 +192,111 @@ describe Gitlab::Ci::Config::External::Processor do
HEREDOC
end
- it 'should take precedence' do
- WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
+ it 'takes precedence' do
+ stub_full_request(remote_file).to_return(body: remote_file_content)
+
expect(processor.perform[:image]).to eq('ruby:2.2')
end
end
+
+ context "when a nested includes are defined" do
+ let(:values) do
+ {
+ include: [
+ { local: '/local/file.yml' }
+ ],
+ image: 'ruby:2.2'
+ }
+ end
+
+ before do
+ allow(project.repository).to receive(:blob_data_at).with('12345', '/local/file.yml') do
+ <<~HEREDOC
+ include:
+ - template: Ruby.gitlab-ci.yml
+ - remote: http://my.domain.com/config.yml
+ - project: #{another_project.full_path}
+ file: /templates/my-workflow.yml
+ HEREDOC
+ end
+
+ allow_any_instance_of(Repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-workflow.yml') do
+ <<~HEREDOC
+ include:
+ - local: /templates/my-build.yml
+ HEREDOC
+ end
+
+ allow_any_instance_of(Repository).to receive(:blob_data_at).with(another_project.commit.id, '/templates/my-build.yml') do
+ <<~HEREDOC
+ my_build:
+ script: echo Hello World
+ HEREDOC
+ end
+
+ stub_full_request('http://my.domain.com/config.yml')
+ .to_return(body: 'remote_build: { script: echo Hello World }')
+ end
+
+ context 'when project is public' do
+ before do
+ another_project.update!(visibility: 'public')
+ end
+
+ it 'properly expands all includes' do
+ is_expected.to include(:my_build, :remote_build, :rspec)
+ end
+ end
+
+ context 'when user is reporter of another project' do
+ before do
+ another_project.add_reporter(user)
+ end
+
+ it 'properly expands all includes' do
+ is_expected.to include(:my_build, :remote_build, :rspec)
+ end
+ end
+
+ context 'when user is not allowed' do
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError, /not found or access denied/)
+ end
+ end
+
+ context 'when too many includes is included' do
+ before do
+ stub_const('Gitlab::Ci::Config::External::Mapper::MAX_INCLUDES', 1)
+ end
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError, /Maximum of 1 nested/)
+ end
+ end
+ end
+
+ context 'when config includes an external configuration file via SSL web request' do
+ before do
+ stub_full_request('https://sha256.badssl.com/fake.yml', ip_address: '8.8.8.8')
+ .to_return(body: 'image: ruby:2.6', status: 200)
+
+ stub_full_request('https://self-signed.badssl.com/fake.yml', ip_address: '8.8.8.9')
+ .to_raise(OpenSSL::SSL::SSLError.new('SSL_connect returned=1 errno=0 state=error: certificate verify failed (self signed certificate)'))
+ end
+
+ context 'with an acceptable certificate' do
+ let(:values) { { include: 'https://sha256.badssl.com/fake.yml' } }
+
+ it { is_expected.to include(image: 'ruby:2.6') }
+ end
+
+ context 'with a self-signed certificate' do
+ let(:values) { { include: 'https://self-signed.badssl.com/fake.yml' } }
+
+ it 'returns a reportable configuration error' do
+ expect { subject }.to raise_error(described_class::IncludeError, /certificate verify failed/)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb
index 18f255c1ab7..7f336ee853e 100644
--- a/spec/lib/gitlab/ci/config_spec.rb
+++ b/spec/lib/gitlab/ci/config_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::Ci::Config do
+ include StubRequests
+
set(:user) { create(:user) }
let(:config) do
@@ -123,6 +125,63 @@ describe Gitlab::Ci::Config do
)
end
end
+
+ context 'when ports have been set' do
+ context 'in the main image' do
+ let(:yml) do
+ <<-EOS
+ image:
+ name: ruby:2.2
+ ports:
+ - 80
+ EOS
+ end
+
+ it 'raises an error' do
+ expect(config.errors).to include("image config contains disallowed keys: ports")
+ end
+ end
+
+ context 'in the job image' do
+ let(:yml) do
+ <<-EOS
+ image: ruby:2.2
+
+ test:
+ script: rspec
+ image:
+ name: ruby:2.2
+ ports:
+ - 80
+ EOS
+ end
+
+ it 'raises an error' do
+ expect(config.errors).to include("jobs:test:image config contains disallowed keys: ports")
+ end
+ end
+
+ context 'in the services' do
+ let(:yml) do
+ <<-EOS
+ image: ruby:2.2
+
+ test:
+ script: rspec
+ image: ruby:2.2
+ services:
+ - name: test
+ alias: test
+ ports:
+ - 80
+ EOS
+ end
+
+ it 'raises an error' do
+ expect(config.errors).to include("jobs:test:services:service config contains disallowed keys: ports")
+ end
+ end
+ end
end
context "when using 'include' directive" do
@@ -133,7 +192,6 @@ describe Gitlab::Ci::Config do
let(:remote_file_content) do
<<~HEREDOC
variables:
- AUTO_DEVOPS_DOMAIN: domain.example.com
POSTGRES_USER: user
POSTGRES_PASSWORD: testing-password
POSTGRES_ENABLED: "true"
@@ -160,22 +218,20 @@ describe Gitlab::Ci::Config do
end
before do
- WebMock.stub_request(:get, remote_location)
- .to_return(body: remote_file_content)
+ stub_full_request(remote_location).to_return(body: remote_file_content)
allow(project.repository)
.to receive(:blob_data_at).and_return(local_file_content)
end
context "when gitlab_ci_yml has valid 'include' defined" do
- it 'should return a composed hash' do
+ it 'returns a composed hash' do
before_script_values = [
"apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs", "ruby -v",
"which ruby",
"bundle install --jobs $(nproc) \"${FLAGS[@]}\""
]
variables = {
- AUTO_DEVOPS_DOMAIN: "domain.example.com",
POSTGRES_USER: "user",
POSTGRES_PASSWORD: "testing-password",
POSTGRES_ENABLED: "true",
@@ -259,7 +315,7 @@ describe Gitlab::Ci::Config do
HEREDOC
end
- it 'should take precedence' do
+ it 'takes precedence' do
expect(config.to_hash).to eq({ image: 'ruby:2.2' })
end
end
@@ -284,7 +340,7 @@ describe Gitlab::Ci::Config do
HEREDOC
end
- it 'should merge the variables dictionaries' do
+ it 'merges the variables dictionaries' do
expect(config.to_hash).to eq({ variables: { A: 'alpha', B: 'beta', C: 'gamma', D: 'delta' } })
end
end
diff --git a/spec/lib/gitlab/ci/cron_parser_spec.rb b/spec/lib/gitlab/ci/cron_parser_spec.rb
index 2a3f7807fdb..491e3fba9d9 100644
--- a/spec/lib/gitlab/ci/cron_parser_spec.rb
+++ b/spec/lib/gitlab/ci/cron_parser_spec.rb
@@ -174,6 +174,13 @@ describe Gitlab::Ci::CronParser do
it { expect(subject).to be_nil }
end
+
+ context 'when cron is scheduled to a non existent day' do
+ let(:cron) { '0 12 31 2 *' }
+ let(:cron_timezone) { 'UTC' }
+
+ it { expect(subject).to be_nil }
+ end
end
describe '#cron_valid?' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
index fab071405df..3debd42ac65 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/build_spec.rb
@@ -96,11 +96,13 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
context 'when pipeline is running for a merge request' do
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
- source: :merge_request,
+ source: :merge_request_event,
origin_ref: 'feature',
checkout_sha: project.commit.id,
after_sha: nil,
before_sha: nil,
+ source_sha: merge_request.diff_head_sha,
+ target_sha: merge_request.target_branch_sha,
trigger_request: nil,
schedule: nil,
merge_request: merge_request,
@@ -115,8 +117,13 @@ describe Gitlab::Ci::Pipeline::Chain::Build do
end
it 'correctly indicated that this is a merge request pipeline' do
- expect(pipeline).to be_merge_request
+ expect(pipeline).to be_merge_request_event
expect(pipeline.merge_request).to eq(merge_request)
end
+
+ it 'correctly sets souce sha and target sha to pipeline' do
+ expect(pipeline.source_sha).to eq(merge_request.diff_head_sha)
+ expect(pipeline.target_sha).to eq(merge_request.target_branch_sha)
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
index 6aa802ce6fd..5181e9c1583 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb
@@ -48,6 +48,24 @@ describe Gitlab::Ci::Pipeline::Chain::Command do
end
end
+ describe '#merge_request_ref_exists?' do
+ subject { command.merge_request_ref_exists? }
+
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ context 'for existing merge request ref' do
+ let(:origin_ref) { merge_request.ref_path }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'for branch ref' do
+ let(:origin_ref) { merge_request.source_branch }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
describe '#ref' do
subject { command.ref }
@@ -161,6 +179,54 @@ describe Gitlab::Ci::Pipeline::Chain::Command do
end
end
+ describe '#source_sha' do
+ subject { command.source_sha }
+
+ let(:command) do
+ described_class.new(project: project,
+ source_sha: source_sha,
+ merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project)
+ end
+
+ let(:source_sha) { nil }
+
+ context 'when source_sha is specified' do
+ let(:source_sha) { 'abc' }
+
+ it 'returns the specified value' do
+ is_expected.to eq('abc')
+ end
+ end
+ end
+
+ describe '#target_sha' do
+ subject { command.target_sha }
+
+ let(:command) do
+ described_class.new(project: project,
+ target_sha: target_sha,
+ merge_request: merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project)
+ end
+
+ let(:target_sha) { nil }
+
+ context 'when target_sha is specified' do
+ let(:target_sha) { 'abc' }
+
+ it 'returns the specified value' do
+ is_expected.to eq('abc')
+ end
+ end
+ end
+
describe '#protected_ref?' do
let(:command) { described_class.new(project: project, origin_ref: 'my-branch') }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
index dc13cae961c..c7f4fc98ca3 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/skip_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do
step.perform!
end
- it 'should break the chain' do
+ it 'breaks the chain' do
expect(step.break?).to be true
end
@@ -37,11 +37,11 @@ describe Gitlab::Ci::Pipeline::Chain::Skip do
step.perform!
end
- it 'should not break the chain' do
+ it 'does not break the chain' do
expect(step.break?).to be false
end
- it 'should not skip a pipeline chain' do
+ it 'does not skip a pipeline chain' do
expect(pipeline.reload).not_to be_skipped
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
index 8ba56d73838..7d750877d09 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/abilities_spec.rb
@@ -10,12 +10,33 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
- project: project, current_user: user, origin_ref: ref)
+ project: project, current_user: user, origin_ref: origin_ref, merge_request: merge_request)
end
let(:step) { described_class.new(pipeline, command) }
let(:ref) { 'master' }
+ let(:origin_ref) { ref }
+ let(:merge_request) { nil }
+
+ shared_context 'detached merge request pipeline' do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project,
+ source_branch: ref,
+ target_project: project,
+ target_branch: 'feature')
+ end
+
+ let(:pipeline) do
+ build(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request,
+ project: project)
+ end
+
+ let(:origin_ref) { merge_request.ref_path }
+ end
context 'when users has no ability to run a pipeline' do
before do
@@ -58,6 +79,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
it { is_expected.to be_truthy }
+ context 'when pipeline is a detached merge request pipeline' do
+ include_context 'detached merge request pipeline'
+
+ it { is_expected.to be_truthy }
+ end
+
context 'when the branch is protected' do
let!(:protected_branch) do
create(:protected_branch, project: project, name: ref)
@@ -65,6 +92,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
it { is_expected.to be_falsey }
+ context 'when pipeline is a detached merge request pipeline' do
+ include_context 'detached merge request pipeline'
+
+ it { is_expected.to be_falsey }
+ end
+
context 'when developers are allowed to merge' do
let!(:protected_branch) do
create(:protected_branch,
@@ -74,6 +107,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
end
it { is_expected.to be_truthy }
+
+ context 'when pipeline is a detached merge request pipeline' do
+ include_context 'detached merge request pipeline'
+
+ it { is_expected.to be_truthy }
+ end
end
end
@@ -112,6 +151,12 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
end
it { is_expected.to be_truthy }
+
+ context 'when pipeline is a detached merge request pipeline' do
+ include_context 'detached merge request pipeline'
+
+ it { is_expected.to be_truthy }
+ end
end
context 'when the tag is protected' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
index 053bc421649..e6c6a82b463 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/config_spec.rb
@@ -115,7 +115,7 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
let(:pipeline) { build_stubbed(:ci_pipeline, project: project) }
let(:merge_request_pipeline) do
- build(:ci_pipeline, source: :merge_request, project: project)
+ build(:ci_pipeline, source: :merge_request_event, project: project)
end
let(:chain) { described_class.new(merge_request_pipeline, command).tap(&:perform!) }
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
index a7cad423d09..2e8c9d70098 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/repository_spec.rb
@@ -42,6 +42,25 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
end
end
+ context 'when origin ref is a merge request ref' do
+ let(:command) do
+ Gitlab::Ci::Pipeline::Chain::Command.new(
+ project: project, current_user: user, origin_ref: origin_ref, checkout_sha: checkout_sha)
+ end
+
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:origin_ref) { merge_request.ref_path }
+ let(:checkout_sha) { project.repository.commit(merge_request.ref_path).id }
+
+ it 'does not break the chain' do
+ expect(step.break?).to be false
+ end
+
+ it 'does not append pipeline errors' do
+ expect(pipeline.errors).to be_empty
+ end
+ end
+
context 'when ref is ambiguous' do
let(:project) do
create(:project, :repository).tap do |proj|
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb
new file mode 100644
index 00000000000..006ce4d8078
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb
@@ -0,0 +1,77 @@
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::And do
+ let(:left) { double('left', evaluate: nil) }
+ let(:right) { double('right', evaluate: nil) }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('&&', left, right)).to be_a(described_class)
+ end
+
+ context 'with non-evaluable operands' do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ it 'raises an operator error' do
+ expect { described_class.build('&&', left, right) }.to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
+ describe '#evaluate' do
+ let(:operator) { described_class.new(left, right) }
+
+ subject { operator.evaluate }
+
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
+ end
+
+ context 'when left and right are truthy' do
+ where(:left_value, :right_value) do
+ [true, 1, 'a'].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_truthy }
+ it { is_expected.to eq(right_value) }
+ end
+ end
+
+ context 'when left or right is falsey' do
+ where(:left_value, :right_value) do
+ [true, false, nil].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when left and right are falsey' do
+ where(:left_value, :right_value) do
+ [false, nil].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_falsey }
+ it { is_expected.to eq(left_value) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
index 019a2ed184d..fcbd2863289 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
@@ -5,9 +5,21 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
let(:right) { double('right') }
describe '.build' do
- it 'creates a new instance of the token' do
- expect(described_class.build('==', left, right))
- .to be_a(described_class)
+ context 'with non-evaluable operands' do
+ it 'creates a new instance of the token' do
+ expect { described_class.build('==', left, right) }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'with evaluable operands' do
+ it 'creates a new instance of the token' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate).and_return('my-string')
+
+ expect(described_class.build('==', left, right))
+ .to be_a(described_class)
+ end
end
end
@@ -17,23 +29,40 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
end
end
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
describe '#evaluate' do
- it 'returns false when left and right are not equal' do
- allow(left).to receive(:evaluate).and_return(1)
- allow(right).to receive(:evaluate).and_return(2)
+ let(:operator) { described_class.new(left, right) }
- operator = described_class.new(left, right)
+ subject { operator.evaluate }
- expect(operator.evaluate(VARIABLE: 3)).to eq false
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
end
- it 'returns true when left and right are equal' do
- allow(left).to receive(:evaluate).and_return(1)
- allow(right).to receive(:evaluate).and_return(1)
+ context 'when left and right are equal' do
+ where(:left_value, :right_value) do
+ [%w(string string)]
+ end
+
+ with_them do
+ it { is_expected.to eq(true) }
+ end
+ end
- operator = described_class.new(left, right)
+ context 'when left and right are not equal' do
+ where(:left_value, :right_value) do
+ ['one string', 'two string'].permutation(2).to_a
+ end
- expect(operator.evaluate(VARIABLE: 3)).to eq true
+ with_them do
+ it { is_expected.to eq(false) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
index 49e5af52f4d..97da66d2bcc 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
@@ -6,9 +6,21 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
let(:right) { double('right') }
describe '.build' do
- it 'creates a new instance of the token' do
- expect(described_class.build('=~', left, right))
- .to be_a(described_class)
+ context 'with non-evaluable operands' do
+ it 'creates a new instance of the token' do
+ expect { described_class.build('=~', left, right) }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'with evaluable operands' do
+ it 'creates a new instance of the token' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate).and_return('/my-string/')
+
+ expect(described_class.build('=~', left, right))
+ .to be_a(described_class)
+ end
end
end
@@ -18,63 +30,93 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
end
end
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
describe '#evaluate' do
- it 'returns false when left and right do not match' do
- allow(left).to receive(:evaluate).and_return('my-string')
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('something'))
+ let(:operator) { described_class.new(left, right) }
- operator = described_class.new(left, right)
+ subject { operator.evaluate }
- expect(operator.evaluate).to eq false
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
end
- it 'returns true when left and right match' do
- allow(left).to receive(:evaluate).and_return('my-awesome-string')
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('awesome.string$'))
+ context 'when left and right do not match' do
+ let(:left_value) { 'my-string' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('something') }
- operator = described_class.new(left, right)
-
- expect(operator.evaluate).to eq true
+ it { is_expected.to eq(nil) }
end
- it 'supports matching against a nil value' do
- allow(left).to receive(:evaluate).and_return(nil)
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('pattern'))
+ context 'when left and right match' do
+ let(:left_value) { 'my-awesome-string' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('awesome.string$') }
+
+ it { is_expected.to eq(3) }
+ end
- operator = described_class.new(left, right)
+ context 'when left is nil' do
+ let(:left_value) { nil }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') }
- expect(operator.evaluate).to eq false
+ it { is_expected.to eq(nil) }
end
- it 'supports multiline strings' do
- allow(left).to receive(:evaluate).and_return <<~TEXT
- My awesome contents
+ context 'when left is a multiline string and matches right' do
+ let(:left_value) do
+ <<~TEXT
+ My awesome contents
+
+ My-text-string!
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') }
+
+ it { is_expected.to eq(24) }
+ end
- My-text-string!
- TEXT
+ context 'when left is a multiline string and does not match right' do
+ let(:left_value) do
+ <<~TEXT
+ My awesome contents
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('text-string'))
+ My-terrible-string!
+ TEXT
+ end
- operator = described_class.new(left, right)
+ let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') }
- expect(operator.evaluate).to eq true
+ it { is_expected.to eq(nil) }
end
- it 'supports regexp flags' do
- allow(left).to receive(:evaluate).and_return <<~TEXT
- My AWESOME content
- TEXT
+ context 'when a matching pattern uses regex flags' do
+ let(:left_value) do
+ <<~TEXT
+ My AWESOME content
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)awesome') }
+
+ it { is_expected.to eq(3) }
+ end
- allow(right).to receive(:evaluate)
- .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome'))
+ context 'when a non-matching pattern uses regex flags' do
+ let(:left_value) do
+ <<~TEXT
+ My AWESOME content
+ TEXT
+ end
- operator = described_class.new(left, right)
+ let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)terrible') }
- expect(operator.evaluate).to eq true
+ it { is_expected.to eq(nil) }
end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
new file mode 100644
index 00000000000..38d30c9035a
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ describe '.build' do
+ context 'with non-evaluable operands' do
+ it 'creates a new instance of the token' do
+ expect { described_class.build('!=', left, right) }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'with evaluable operands' do
+ it 'creates a new instance of the token' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate).and_return('my-string')
+
+ expect(described_class.build('!=', left, right))
+ .to be_a(described_class)
+ end
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
+ describe '#evaluate' do
+ let(:operator) { described_class.new(left, right) }
+
+ subject { operator.evaluate }
+
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
+ end
+
+ context 'when left and right are equal' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:left_value, :right_value) do
+ 'string' | 'string'
+ 1 | 1
+ '' | ''
+ nil | nil
+ end
+
+ with_them do
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when left and right are not equal' do
+ where(:left_value, :right_value) do
+ ['one string', 'two string', 1, 2, '', nil, false, true].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to eq(true) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
new file mode 100644
index 00000000000..99110ff8d88
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
@@ -0,0 +1,122 @@
+require 'fast_spec_helper'
+require_dependency 're2'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ describe '.build' do
+ context 'with non-evaluable operands' do
+ it 'creates a new instance of the token' do
+ expect { described_class.build('!~', left, right) }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'with evaluable operands' do
+ it 'creates a new instance of the token' do
+ allow(left).to receive(:evaluate).and_return('my-string')
+ allow(right).to receive(:evaluate).and_return('my-string')
+
+ expect(described_class.build('!~', left, right))
+ .to be_a(described_class)
+ end
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
+ describe '#evaluate' do
+ let(:operator) { described_class.new(left, right) }
+
+ subject { operator.evaluate }
+
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
+ end
+
+ context 'when left and right do not match' do
+ let(:left_value) { 'my-string' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('something') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when left and right match' do
+ let(:left_value) { 'my-awesome-string' }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('awesome.string$') }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when left is nil' do
+ let(:left_value) { nil }
+ let(:right_value) { Gitlab::UntrustedRegexp.new('pattern') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when left is a multiline string and matches right' do
+ let(:left_value) do
+ <<~TEXT
+ My awesome contents
+
+ My-text-string!
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when left is a multiline string and does not match right' do
+ let(:left_value) do
+ <<~TEXT
+ My awesome contents
+
+ My-terrible-string!
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('text-string') }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when a matching pattern uses regex flags' do
+ let(:left_value) do
+ <<~TEXT
+ My AWESOME content
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)awesome') }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when a non-matching pattern uses regex flags' do
+ let(:left_value) do
+ <<~TEXT
+ My AWESOME content
+ TEXT
+ end
+
+ let(:right_value) { Gitlab::UntrustedRegexp.new('(?i)terrible') }
+
+ it { is_expected.to eq(true) }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb
new file mode 100644
index 00000000000..d542eebc613
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb
@@ -0,0 +1,77 @@
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Or do
+ let(:left) { double('left', evaluate: nil) }
+ let(:right) { double('right', evaluate: nil) }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('||', left, right)).to be_a(described_class)
+ end
+
+ context 'with non-evaluable operands' do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ it 'raises an operator error' do
+ expect { described_class.build('||', left, right) }.to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '.precedence' do
+ it 'has a precedence' do
+ expect(described_class.precedence).to be_an Integer
+ end
+ end
+
+ describe '#evaluate' do
+ let(:operator) { described_class.new(left, right) }
+
+ subject { operator.evaluate }
+
+ before do
+ allow(left).to receive(:evaluate).and_return(left_value)
+ allow(right).to receive(:evaluate).and_return(right_value)
+ end
+
+ context 'when left and right are truthy' do
+ where(:left_value, :right_value) do
+ [true, 1, 'a'].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_truthy }
+ it { is_expected.to eq(left_value) }
+ end
+ end
+
+ context 'when left or right is truthy' do
+ where(:left_value, :right_value) do
+ [true, false, 'a'].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when left and right are falsey' do
+ where(:left_value, :right_value) do
+ [false, nil].permutation(2).to_a
+ end
+
+ with_them do
+ it { is_expected.to be_falsey }
+ it { is_expected.to eq(right_value) }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
index 3ebc2e94727..30ea3f3e28e 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/pattern_spec.rb
@@ -1,4 +1,4 @@
-require 'fast_spec_helper'
+require 'spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
describe '.build' do
@@ -30,16 +30,6 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
.to eq Gitlab::UntrustedRegexp.new('pattern')
end
- it 'is a greedy scanner for regexp boundaries' do
- scanner = StringScanner.new('/some .* / pattern/')
-
- token = described_class.scan(scanner)
-
- expect(token).not_to be_nil
- expect(token.build.evaluate)
- .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
- end
-
it 'does not allow to use an empty pattern' do
scanner = StringScanner.new(%(//))
@@ -68,12 +58,90 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
.to eq Gitlab::UntrustedRegexp.new('(?im)pattern')
end
- it 'does not support arbitrary flags' do
+ it 'ignores unsupported flags' do
scanner = StringScanner.new('/pattern/x')
token = described_class.scan(scanner)
- expect(token).to be_nil
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('pattern')
+ end
+
+ it 'is a eager scanner for regexp boundaries' do
+ scanner = StringScanner.new('/some .* / pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* ')
+ end
+
+ it 'does not match on escaped regexp boundaries' do
+ scanner = StringScanner.new('/some .* \/ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
+ end
+
+ it 'recognizes \ as an escape character for /' do
+ scanner = StringScanner.new('/some numeric \/$ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some numeric /$ pattern')
+ end
+
+ it 'does not recognize \ as an escape character for $' do
+ scanner = StringScanner.new('/some numeric \$ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some numeric \$ pattern')
+ end
+
+ context 'with the ci_variables_complex_expressions feature flag disabled' do
+ before do
+ stub_feature_flags(ci_variables_complex_expressions: false)
+ end
+
+ it 'is a greedy scanner for regexp boundaries' do
+ scanner = StringScanner.new('/some .* / pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* / pattern')
+ end
+
+ it 'does not recognize the \ escape character for /' do
+ scanner = StringScanner.new('/some .* \/ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some .* \/ pattern')
+ end
+
+ it 'does not recognize the \ escape character for $' do
+ scanner = StringScanner.new('/some numeric \$ pattern/')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate)
+ .to eq Gitlab::UntrustedRegexp.new('some numeric \$ pattern')
+ end
end
end
@@ -85,7 +153,7 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::Pattern do
end
it 'raises error if evaluated regexp is not valid' do
- allow(Gitlab::UntrustedRegexp).to receive(:valid?).and_return(true)
+ allow(Gitlab::UntrustedRegexp::RubySyntax).to receive(:valid?).and_return(true)
regexp = described_class.new('/invalid ( .*/')
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
index 3f11b3f7673..d8db9c262a1 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
@@ -58,6 +58,56 @@ describe Gitlab::Ci::Pipeline::Expression::Lexer do
expect { lexer.tokens }
.to raise_error described_class::SyntaxError
end
+
+ context 'with complex expressions' do
+ using RSpec::Parameterized::TableSyntax
+
+ subject { described_class.new(expression).tokens.map(&:value) }
+
+ where(:expression, :tokens) do
+ '$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/' | ['$PRESENT_VARIABLE', '=~', '/my var/', '&&', '$EMPTY_VARIABLE', '=~', '/nope/']
+ '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE' | ['$EMPTY_VARIABLE', '==', '""', '&&', '$PRESENT_VARIABLE']
+ '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE != "nope"' | ['$EMPTY_VARIABLE', '==', '""', '&&', '$PRESENT_VARIABLE', '!=', '"nope"']
+ '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | ['$PRESENT_VARIABLE', '&&', '$EMPTY_VARIABLE']
+ '$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/' | ['$PRESENT_VARIABLE', '=~', '/my var/', '||', '$EMPTY_VARIABLE', '=~', '/nope/']
+ '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE' | ['$EMPTY_VARIABLE', '==', '""', '||', '$PRESENT_VARIABLE']
+ '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE != "nope"' | ['$EMPTY_VARIABLE', '==', '""', '||', '$PRESENT_VARIABLE', '!=', '"nope"']
+ '$PRESENT_VARIABLE || $EMPTY_VARIABLE' | ['$PRESENT_VARIABLE', '||', '$EMPTY_VARIABLE']
+ '$PRESENT_VARIABLE && null || $EMPTY_VARIABLE == ""' | ['$PRESENT_VARIABLE', '&&', 'null', '||', '$EMPTY_VARIABLE', '==', '""']
+ end
+
+ with_them do
+ it { is_expected.to eq(tokens) }
+ end
+ end
+
+ context 'with the ci_variables_complex_expressions feature flag turned off' do
+ before do
+ stub_feature_flags(ci_variables_complex_expressions: false)
+ end
+
+ it 'incorrectly tokenizes conjunctive match statements as one match statement' do
+ tokens = described_class.new('$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/').tokens
+
+ expect(tokens.map(&:value)).to eq(['$PRESENT_VARIABLE', '=~', '/my var/ && $EMPTY_VARIABLE =~ /nope/'])
+ end
+
+ it 'incorrectly tokenizes disjunctive match statements as one statement' do
+ tokens = described_class.new('$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/').tokens
+
+ expect(tokens.map(&:value)).to eq(['$PRESENT_VARIABLE', '=~', '/my var/ || $EMPTY_VARIABLE =~ /nope/'])
+ end
+
+ it 'raises an error about && operators' do
+ expect { described_class.new('$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE').tokens }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError).with_message('Unknown lexeme found!')
+ end
+
+ it 'raises an error about || operators' do
+ expect { described_class.new('$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE').tokens }
+ .to raise_error(Gitlab::Ci::Pipeline::Expression::Lexer::SyntaxError).with_message('Unknown lexeme found!')
+ end
+ end
end
describe '#lexemes' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
index 2b78b1dd4a7..e88ec5561b6 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
@@ -2,25 +2,67 @@ require 'fast_spec_helper'
describe Gitlab::Ci::Pipeline::Expression::Parser do
describe '#tree' do
- context 'when using operators' do
+ context 'when using two operators' do
+ it 'returns a reverse descent parse tree' do
+ expect(described_class.seed('$VAR1 == "123"').tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
+ end
+ end
+
+ context 'when using three operators' do
it 'returns a reverse descent parse tree' do
expect(described_class.seed('$VAR1 == "123" == $VAR2').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
end
end
- context 'when using a single token' do
+ context 'when using a single variable token' do
it 'returns a single token instance' do
expect(described_class.seed('$VAR').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable
end
end
+ context 'when using a single string token' do
+ it 'returns a single token instance' do
+ expect(described_class.seed('"some value"').tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::String
+ end
+ end
+
context 'when expression is empty' do
it 'returns a null token' do
- expect(described_class.seed('').tree)
+ expect { described_class.seed('').tree }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Parser::ParseError
+ end
+ end
+
+ context 'when expression is null' do
+ it 'returns a null token' do
+ expect(described_class.seed('null').tree)
.to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Null
end
end
+
+ context 'when two value tokens have no operator' do
+ it 'raises a parsing error' do
+ expect { described_class.seed('$VAR "text"').tree }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Parser::ParseError
+ end
+ end
+
+ context 'when an operator has no left side' do
+ it 'raises an OperatorError' do
+ expect { described_class.seed('== "123"').tree }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
+
+ context 'when an operator has no right side' do
+ it 'raises an OperatorError' do
+ expect { described_class.seed('$VAR ==').tree }
+ .to raise_error Gitlab::Ci::Pipeline::Expression::Lexeme::Operator::OperatorError
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index 11e73294f18..057e2f3fbe8 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -1,4 +1,6 @@
-require 'fast_spec_helper'
+# TODO switch this back after the "ci_variables_complex_expressions" feature flag is removed
+# require 'fast_spec_helper'
+require 'spec_helper'
require 'rspec-parameterized'
describe Gitlab::Ci::Pipeline::Expression::Statement do
@@ -7,8 +9,12 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
let(:variables) do
- { 'PRESENT_VARIABLE' => 'my variable',
- EMPTY_VARIABLE: '' }
+ {
+ 'PRESENT_VARIABLE' => 'my variable',
+ 'PATH_VARIABLE' => 'a/path/variable/value',
+ 'FULL_PATH_VARIABLE' => '/a/full/path/variable/value',
+ 'EMPTY_VARIABLE' => ''
+ }
end
describe '.new' do
@@ -21,93 +27,158 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
end
end
- describe '#parse_tree' do
- context 'when expression is empty' do
- let(:text) { '' }
-
- it 'raises an error' do
- expect { subject.parse_tree }
- .to raise_error described_class::StatementError
- end
- end
+ describe '#evaluate' do
+ using RSpec::Parameterized::TableSyntax
- context 'when expression grammar is incorrect' do
- table = [
- '$VAR "text"', # missing operator
- '== "123"', # invalid left side
- '"some string"', # only string provided
- '$VAR ==', # invalid right side
- 'null', # missing lexemes
- '' # empty statement
- ]
-
- table.each do |syntax|
- context "when expression grammar is #{syntax.inspect}" do
- let(:text) { syntax }
-
- it 'raises a statement error exception' do
- expect { subject.parse_tree }
- .to raise_error described_class::StatementError
- end
-
- it 'is an invalid statement' do
- expect(subject).not_to be_valid
- end
- end
- end
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ '"my variable" == $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE == null' | false
+ '$EMPTY_VARIABLE == null' | false
+ '"" == $EMPTY_VARIABLE' | true
+ '$EMPTY_VARIABLE' | ''
+ '$UNDEFINED_VARIABLE == null' | true
+ 'null == $UNDEFINED_VARIABLE' | true
+ '$PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | 3
+ '$PRESENT_VARIABLE =~ /va\r.*e$/' | nil
+ '$PRESENT_VARIABLE =~ /va\/r.*e$/' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | 3
+ "$PRESENT_VARIABLE =~ /^var.*/" | nil
+ "$EMPTY_VARIABLE =~ /var.*/" | nil
+ "$UNDEFINED_VARIABLE =~ /var.*/" | nil
+ "$PRESENT_VARIABLE =~ /VAR.*/i" | 3
+ '$PATH_VARIABLE =~ /path\/variable/' | 2
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/' | 0
+ '$FULL_PATH_VARIABLE =~ /\\/path\\/variable\\/value$/' | 7
+ '$PRESENT_VARIABLE != "my variable"' | false
+ '"my variable" != $PRESENT_VARIABLE' | false
+ '$PRESENT_VARIABLE != null' | true
+ '$EMPTY_VARIABLE != null' | true
+ '"" != $EMPTY_VARIABLE' | false
+ '$UNDEFINED_VARIABLE != null' | false
+ 'null != $UNDEFINED_VARIABLE' | false
+ "$PRESENT_VARIABLE !~ /var.*e$/" | false
+ "$PRESENT_VARIABLE !~ /^var.*/" | true
+ '$PRESENT_VARIABLE !~ /^v\ar.*/' | true
+ '$PRESENT_VARIABLE !~ /^v\/ar.*/' | true
+ "$EMPTY_VARIABLE !~ /var.*/" | true
+ "$UNDEFINED_VARIABLE !~ /var.*/" | true
+ "$PRESENT_VARIABLE !~ /VAR.*/i" | false
+
+ '$PRESENT_VARIABLE && "string"' | 'string'
+ '$PRESENT_VARIABLE && $PRESENT_VARIABLE' | 'my variable'
+ '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | ''
+ '$PRESENT_VARIABLE && null' | nil
+ '"string" && $PRESENT_VARIABLE' | 'my variable'
+ '$EMPTY_VARIABLE && $PRESENT_VARIABLE' | 'my variable'
+ 'null && $PRESENT_VARIABLE' | nil
+ '$EMPTY_VARIABLE && "string"' | 'string'
+ '$EMPTY_VARIABLE && $EMPTY_VARIABLE' | ''
+ '"string" && $EMPTY_VARIABLE' | ''
+ '"string" && null' | nil
+ 'null && "string"' | nil
+ '"string" && "string"' | 'string'
+ 'null && null' | nil
+
+ '$PRESENT_VARIABLE =~ /my var/ && $EMPTY_VARIABLE =~ /nope/' | nil
+ '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE' | 'my variable'
+ '$EMPTY_VARIABLE == "" && $PRESENT_VARIABLE != "nope"' | true
+ '$PRESENT_VARIABLE && $EMPTY_VARIABLE' | ''
+ '$PRESENT_VARIABLE && $UNDEFINED_VARIABLE' | nil
+ '$UNDEFINED_VARIABLE && $EMPTY_VARIABLE' | nil
+ '$UNDEFINED_VARIABLE && $PRESENT_VARIABLE' | nil
+
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ && $PATH_VARIABLE =~ /path\/variable/' | 2
+ '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ && $PATH_VARIABLE =~ /path\/variable/' | nil
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ && $PATH_VARIABLE =~ /bad\/path\/variable/' | nil
+ '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ && $PATH_VARIABLE =~ /bad\/path\/variable/' | nil
+
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ || $PATH_VARIABLE =~ /path\/variable/' | 0
+ '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ || $PATH_VARIABLE =~ /path\/variable/' | 2
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/ || $PATH_VARIABLE =~ /bad\/path\/variable/' | 0
+ '$FULL_PATH_VARIABLE =~ /^\/a\/bad\/path\/variable\/value$/ || $PATH_VARIABLE =~ /bad\/path\/variable/' | nil
+
+ '$PRESENT_VARIABLE =~ /my var/ || $EMPTY_VARIABLE =~ /nope/' | 0
+ '$EMPTY_VARIABLE == "" || $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE != "nope" || $EMPTY_VARIABLE == ""' | true
+
+ '$PRESENT_VARIABLE && null || $EMPTY_VARIABLE == ""' | true
+ '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE || $PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE == null || $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE || $UNDEFINED_VARIABLE == null' | 'my variable'
end
- context 'when expression grammar is correct' do
- context 'when using an operator' do
- let(:text) { '$VAR == "value"' }
-
- it 'returns a reverse descent parse tree' do
- expect(subject.parse_tree)
- .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
- end
+ with_them do
+ let(:text) { expression }
- it 'is a valid statement' do
- expect(subject).to be_valid
- end
+ it "evaluates to `#{params[:value].inspect}`" do
+ expect(subject.evaluate).to eq(value)
end
- context 'when using a single token' do
- let(:text) { '$PRESENT_VARIABLE' }
+ # This test is used to ensure that our parser
+ # returns exactly the same results as if we
+ # were evaluating using ruby's `eval`
+ context 'when using Ruby eval' do
+ let(:expression_ruby) do
+ expression
+ .gsub(/null/, 'nil')
+ .gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)/) { "variables['#{Regexp.last_match(1)}']" }
+ end
- it 'returns a single token instance' do
- expect(subject.parse_tree)
- .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable
+ it 'behaves exactly the same' do
+ expect(instance_eval(expression_ruby)).to eq(subject.evaluate)
end
end
end
- end
- describe '#evaluate' do
- using RSpec::Parameterized::TableSyntax
+ context 'with the ci_variables_complex_expressions feature flag disabled' do
+ before do
+ stub_feature_flags(ci_variables_complex_expressions: false)
+ end
- where(:expression, :value) do
- '$PRESENT_VARIABLE == "my variable"' | true
- '"my variable" == $PRESENT_VARIABLE' | true
- '$PRESENT_VARIABLE == null' | false
- '$EMPTY_VARIABLE == null' | false
- '"" == $EMPTY_VARIABLE' | true
- '$EMPTY_VARIABLE' | ''
- '$UNDEFINED_VARIABLE == null' | true
- 'null == $UNDEFINED_VARIABLE' | true
- '$PRESENT_VARIABLE' | 'my variable'
- '$UNDEFINED_VARIABLE' | nil
- "$PRESENT_VARIABLE =~ /var.*e$/" | true
- "$PRESENT_VARIABLE =~ /^var.*/" | false
- "$EMPTY_VARIABLE =~ /var.*/" | false
- "$UNDEFINED_VARIABLE =~ /var.*/" | false
- "$PRESENT_VARIABLE =~ /VAR.*/i" | true
- end
+ where(:expression, :value) do
+ '$PRESENT_VARIABLE == "my variable"' | true
+ '"my variable" == $PRESENT_VARIABLE' | true
+ '$PRESENT_VARIABLE == null' | false
+ '$EMPTY_VARIABLE == null' | false
+ '"" == $EMPTY_VARIABLE' | true
+ '$EMPTY_VARIABLE' | ''
+ '$UNDEFINED_VARIABLE == null' | true
+ 'null == $UNDEFINED_VARIABLE' | true
+ '$PRESENT_VARIABLE' | 'my variable'
+ '$UNDEFINED_VARIABLE' | nil
+ "$PRESENT_VARIABLE =~ /var.*e$/" | true
+ "$PRESENT_VARIABLE =~ /^var.*/" | false
+ "$EMPTY_VARIABLE =~ /var.*/" | false
+ "$UNDEFINED_VARIABLE =~ /var.*/" | false
+ "$PRESENT_VARIABLE =~ /VAR.*/i" | true
+ '$PATH_VARIABLE =~ /path/variable/' | true
+ '$PATH_VARIABLE =~ /path\/variable/' | true
+ '$FULL_PATH_VARIABLE =~ /^/a/full/path/variable/value$/' | true
+ '$FULL_PATH_VARIABLE =~ /^\/a\/full\/path\/variable\/value$/' | true
+ '$PRESENT_VARIABLE != "my variable"' | false
+ '"my variable" != $PRESENT_VARIABLE' | false
+ '$PRESENT_VARIABLE != null' | true
+ '$EMPTY_VARIABLE != null' | true
+ '"" != $EMPTY_VARIABLE' | false
+ '$UNDEFINED_VARIABLE != null' | false
+ 'null != $UNDEFINED_VARIABLE' | false
+ "$PRESENT_VARIABLE !~ /var.*e$/" | false
+ "$PRESENT_VARIABLE !~ /^var.*/" | true
+ "$EMPTY_VARIABLE !~ /var.*/" | true
+ "$UNDEFINED_VARIABLE !~ /var.*/" | true
+ "$PRESENT_VARIABLE !~ /VAR.*/i" | false
+ end
- with_them do
- let(:text) { expression }
+ with_them do
+ let(:text) { expression }
- it "evaluates to `#{params[:value].inspect}`" do
- expect(subject.evaluate).to eq value
+ it "evaluates to `#{params[:value].inspect}`" do
+ expect(subject.evaluate).to eq value
+ end
end
end
end
@@ -125,6 +196,8 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
'$INVALID = 1' | false
"$PRESENT_VARIABLE =~ /var.*/" | true
"$UNDEFINED_VARIABLE =~ /var.*/" | false
+ "$PRESENT_VARIABLE !~ /var.*/" | false
+ "$UNDEFINED_VARIABLE !~ /var.*/" | true
end
with_them do
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index b379b08ad62..b6231510b91 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -123,6 +123,35 @@ describe Gitlab::Ci::Status::Build::Factory do
expect(status.action_path).to include 'retry'
end
end
+
+ context 'when build has unmet prerequisites' do
+ let(:build) { create(:ci_build, :prerequisite_failure) }
+
+ it 'matches correct core status' do
+ expect(factory.core_status).to be_a Gitlab::Ci::Status::Failed
+ end
+
+ it 'matches correct extended statuses' do
+ expect(factory.extended_statuses)
+ .to eq [Gitlab::Ci::Status::Build::Retryable,
+ Gitlab::Ci::Status::Build::FailedUnmetPrerequisites]
+ end
+
+ it 'fabricates a failed with unmet prerequisites build status' do
+ expect(status).to be_a Gitlab::Ci::Status::Build::FailedUnmetPrerequisites
+ end
+
+ it 'fabricates status with correct details' do
+ expect(status.text).to eq 'failed'
+ expect(status.icon).to eq 'status_failed'
+ expect(status.favicon).to eq 'favicon_status_failed'
+ expect(status.label).to eq 'failed'
+ expect(status).to have_details
+ expect(status).to have_action
+ expect(status.action_title).to include 'Retry'
+ expect(status.action_path).to include 'retry'
+ end
+ end
end
context 'when build is a canceled' do
diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
index af03d5a1308..2a5915d75d0 100644
--- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb
@@ -31,7 +31,7 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do
describe '#group' do
it 'returns status failed with warnings status group' do
- expect(subject.group).to eq 'failed_with_warnings'
+ expect(subject.group).to eq 'failed-with-warnings'
end
end
diff --git a/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb
new file mode 100644
index 00000000000..a4854bdc6b9
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/failed_unmet_prerequisites_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+RSpec.describe Gitlab::Ci::Status::Build::FailedUnmetPrerequisites do
+ describe '#illustration' do
+ subject { described_class.new(double).illustration }
+
+ it { is_expected.to include(:image, :size, :title, :content) }
+ end
+
+ describe '.matches?' do
+ let(:build) { create(:ci_build, :created) }
+
+ subject { described_class.matches?(build, double) }
+
+ context 'when build has not failed' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when build has failed' do
+ before do
+ build.drop!(failure_reason)
+ end
+
+ context 'with unmet prerequisites' do
+ let(:failure_reason) { :unmet_prerequisites }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with a different error' do
+ let(:failure_reason) { :runner_system_failure }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/build/preparing_spec.rb b/spec/lib/gitlab/ci/status/build/preparing_spec.rb
new file mode 100644
index 00000000000..4d8945845ba
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/build/preparing_spec.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Build::Preparing do
+ subject do
+ described_class.new(double('subject'))
+ end
+
+ describe '#illustration' do
+ it { expect(subject.illustration).to include(:image, :size, :title, :content) }
+ end
+
+ describe '.matches?' do
+ subject { described_class.matches?(build, nil) }
+
+ context 'when build is preparing' do
+ let(:build) { create(:ci_build, :preparing) }
+
+ it 'is a correct match' do
+ expect(subject).to be true
+ end
+ end
+
+ context 'when build is not preparing' do
+ let(:build) { create(:ci_build, :success) }
+
+ it 'does not match' do
+ expect(subject).to be false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/preparing_spec.rb b/spec/lib/gitlab/ci/status/preparing_spec.rb
new file mode 100644
index 00000000000..7211c0e506d
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/preparing_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Preparing do
+ subject do
+ described_class.new(double('subject'), nil)
+ end
+
+ describe '#text' do
+ it { expect(subject.text).to eq 'preparing' }
+ end
+
+ describe '#label' do
+ it { expect(subject.label).to eq 'preparing' }
+ end
+
+ describe '#icon' do
+ it { expect(subject.icon).to eq 'status_created' }
+ end
+
+ describe '#favicon' do
+ it { expect(subject.favicon).to eq 'favicon_status_created' }
+ end
+
+ describe '#group' do
+ it { expect(subject.group).to eq 'preparing' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/stage/factory_spec.rb b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
index dee4f4efd1b..4b299170c87 100644
--- a/spec/lib/gitlab/ci/status/stage/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/stage/factory_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Ci::Status::Stage::Factory do
end
context 'when stage has a core status' do
- HasStatus::AVAILABLE_STATUSES.each do |core_status|
+ (HasStatus::AVAILABLE_STATUSES - %w(manual skipped scheduled)).each do |core_status|
context "when core status is #{core_status}" do
before do
create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status)
@@ -64,4 +64,20 @@ describe Gitlab::Ci::Status::Stage::Factory do
expect(status.details_path).to include "pipelines/#{pipeline.id}##{stage.name}"
end
end
+
+ context 'when stage has manual builds' do
+ (HasStatus::BLOCKED_STATUS + ['skipped']).each do |core_status|
+ context "when status is #{core_status}" do
+ before do
+ create(:ci_build, pipeline: pipeline, stage: 'test', status: core_status)
+ create(:commit_status, pipeline: pipeline, stage: 'test', status: core_status)
+ create(:ci_build, pipeline: pipeline, stage: 'build', status: :manual)
+ end
+
+ it 'fabricates a play manual status' do
+ expect(status).to be_a(Gitlab::Ci::Status::Stage::PlayManual)
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb
new file mode 100644
index 00000000000..b0113b00ef0
--- /dev/null
+++ b/spec/lib/gitlab/ci/status/stage/play_manual_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Ci::Status::Stage::PlayManual do
+ let(:stage) { double('stage') }
+ let(:play_manual) { described_class.new(stage) }
+
+ describe '#action_icon' do
+ subject { play_manual.action_icon }
+
+ it { is_expected.to eq('play') }
+ end
+
+ describe '#action_button_title' do
+ subject { play_manual.action_button_title }
+
+ it { is_expected.to eq('Play all manual') }
+ end
+
+ describe '#action_title' do
+ subject { play_manual.action_title }
+
+ it { is_expected.to eq('Play all manual') }
+ end
+
+ describe '#action_path' do
+ let(:stage) { create(:ci_stage_entity, status: 'manual') }
+ let(:pipeline) { stage.pipeline }
+ let(:play_manual) { stage.detailed_status(create(:user)) }
+
+ subject { play_manual.action_path }
+
+ it { is_expected.to eq("/#{pipeline.project.full_path}/pipelines/#{pipeline.id}/stages/#{stage.name}/play_manual") }
+ end
+
+ describe '#action_method' do
+ subject { play_manual.action_method }
+
+ it { is_expected.to eq(:post) }
+ end
+
+ describe '.matches?' do
+ let(:user) { double('user') }
+
+ subject { described_class.matches?(stage, user) }
+
+ context 'when stage is skipped' do
+ let(:stage) { create(:ci_stage_entity, status: :skipped) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when stage is manual' do
+ let(:stage) { create(:ci_stage_entity, status: :manual) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when stage is scheduled' do
+ let(:stage) { create(:ci_stage_entity, status: :scheduled) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when stage is success' do
+ let(:stage) { create(:ci_stage_entity, status: :success) }
+
+ context 'and does not have manual builds' do
+ it { is_expected.to be_falsy }
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb
index 6d05545d1d8..9493b1d89f2 100644
--- a/spec/lib/gitlab/ci/status/success_warning_spec.rb
+++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Ci::Status::SuccessWarning do
end
describe '#group' do
- it { expect(subject.group).to eq 'success_with_warnings' }
+ it { expect(subject.group).to eq 'success-with-warnings' }
end
describe '.matches?' do
diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb
index 0dd74399a47..b52064b3036 100644
--- a/spec/lib/gitlab/ci/templates/templates_spec.rb
+++ b/spec/lib/gitlab/ci/templates/templates_spec.rb
@@ -3,9 +3,32 @@
require 'spec_helper'
describe "CI YML Templates" do
- Gitlab::Template::GitlabCiYmlTemplate.all.each do |template|
- it "#{template.name} should be valid" do
- expect { Gitlab::Ci::YamlProcessor.new(template.content) }.not_to raise_error
+ using RSpec::Parameterized::TableSyntax
+
+ subject { Gitlab::Ci::YamlProcessor.new(content) }
+
+ where(:template_name) do
+ Gitlab::Template::GitlabCiYmlTemplate.all.map(&:full_name)
+ end
+
+ with_them do
+ let(:content) do
+ <<~EOS
+ include:
+ - template: #{template_name}
+
+ concrete_build_implemented_by_a_user:
+ stage: test
+ script: do something
+ EOS
+ end
+
+ it 'is valid' do
+ expect { subject }.not_to raise_error
+ end
+
+ it 'require default stages to be included' do
+ expect(subject.stages).to include(*Gitlab::Ci::Config::Entry::Stages.default)
end
end
end
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index 38626f728d7..e45ea1c2528 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -414,7 +414,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
context 'malicious regexp' do
let(:data) { malicious_text }
- let(:regex) { malicious_regexp }
+ let(:regex) { malicious_regexp_re2 }
include_examples 'malicious regexp'
end
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index 8bf44acb228..613814df23f 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::Ci::Variables::Collection::Item do
let(:expected_value) { variable_value }
let(:variable) do
- { key: variable_key, value: variable_value, public: true }
+ { key: variable_key, value: variable_value, public: true, masked: false }
end
describe '.new' do
@@ -88,7 +88,7 @@ describe Gitlab::Ci::Variables::Collection::Item do
resource = described_class.fabricate(variable)
expect(resource).to be_a(described_class)
- expect(resource).to eq(key: 'CI_VAR', value: '123', public: false)
+ expect(resource).to eq(key: 'CI_VAR', value: '123', public: false, masked: false)
end
it 'supports using another collection item' do
@@ -134,7 +134,7 @@ describe Gitlab::Ci::Variables::Collection::Item do
.to_runner_variable
expect(runner_variable)
- .to eq(key: 'VAR', value: 'value', public: true, file: true)
+ .to eq(key: 'VAR', value: 'value', public: true, file: true, masked: false)
end
end
end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 5c91816a586..8e732d44d5d 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Variables::Collection do
describe '.new' do
it 'can be initialized with an array' do
- variable = { key: 'VAR', value: 'value', public: true }
+ variable = { key: 'VAR', value: 'value', public: true, masked: false }
collection = described_class.new([variable])
@@ -66,6 +66,14 @@ describe Gitlab::Ci::Variables::Collection do
expect(collection).to include(key: 'VAR_3', value: '3', public: true)
end
+ it 'does not concatenate resource if it undefined' do
+ collection = described_class.new([{ key: 'VAR_1', value: '1' }])
+
+ collection.concat(nil)
+
+ expect(collection).to be_one
+ end
+
it 'returns self' do
expect(subject.concat([key: 'VAR', value: 'test']))
.to eq subject
@@ -93,7 +101,7 @@ describe Gitlab::Ci::Variables::Collection do
collection = described_class.new([{ key: 'TEST', value: '1' }])
expect(collection.to_runner_variables)
- .to eq [{ key: 'TEST', value: '1', public: true }]
+ .to eq [{ key: 'TEST', value: '1', public: true, masked: false }]
end
end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index 91139d421f5..635b4e556e8 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
module Gitlab
module Ci
describe YamlProcessor do
+ include StubRequests
+
subject { described_class.new(config, user: nil) }
describe '#build_attributes' do
@@ -602,6 +604,100 @@ module Gitlab
end
end
+ describe "Include" do
+ let(:opts) { {} }
+
+ let(:config) do
+ {
+ include: include_content,
+ rspec: { script: "test" }
+ }
+ end
+
+ subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), opts) }
+
+ context "when validating a ci config file with no project context" do
+ context "when a single string is provided" do
+ let(:include_content) { "/local.gitlab-ci.yml" }
+
+ it "returns a validation error" do
+ expect { subject }.to raise_error /does not have project/
+ end
+ end
+
+ context "when an array is provided" do
+ let(:include_content) { ["/local.gitlab-ci.yml"] }
+
+ it "returns a validation error" do
+ expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, /does not have project/)
+ end
+ end
+
+ context "when an array of wrong keyed object is provided" do
+ let(:include_content) { [{ yolo: "/local.gitlab-ci.yml" }] }
+
+ it "returns a validation error" do
+ expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
+ end
+ end
+
+ context "when an array of mixed typed objects is provided" do
+ let(:include_content) do
+ [
+ 'https://gitlab.com/awesome-project/raw/master/.before-script-template.yml',
+ { template: 'Auto-DevOps.gitlab-ci.yml' }
+ ]
+ end
+
+ before do
+ stub_full_request('https://gitlab.com/awesome-project/raw/master/.before-script-template.yml')
+ .to_return(
+ status: 200,
+ headers: { 'Content-Type' => 'application/json' },
+ body: 'prepare: { script: ls -al }')
+ end
+
+ it "does not return any error" do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context "when the include type is incorrect" do
+ let(:include_content) { { name: "/local.gitlab-ci.yml" } }
+
+ it "returns an invalid configuration error" do
+ expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
+ end
+ end
+ end
+
+ context "when validating a ci config file within a project" do
+ let(:include_content) { "/local.gitlab-ci.yml" }
+ let(:project) { create(:project, :repository) }
+ let(:opts) { { project: project, sha: project.commit.sha } }
+
+ context "when the included internal file is present" do
+ before do
+ expect(project.repository).to receive(:blob_data_at)
+ .and_return(YAML.dump({ job1: { script: 'hello' } }))
+ end
+
+ it "does not return an error" do
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context "when the included internal file is not present" do
+ it "returns an error with missing file details" do
+ expect { subject }.to raise_error(
+ Gitlab::Ci::YamlProcessor::ValidationError,
+ "Local file `#{include_content}` does not exist!"
+ )
+ end
+ end
+ end
+ end
+
describe "When" do
%w(on_success on_failure always).each do |when_state|
it "returns #{when_state} when defined" do
@@ -1154,7 +1250,7 @@ module Gitlab
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services:service config should be a hash or a string")
end
it "returns errors if job services parameter is not an array" do
@@ -1168,7 +1264,7 @@ module Gitlab
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
- end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
+ end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services:service config should be a hash or a string")
end
it "returns error if job configuration is invalid" do
@@ -1374,7 +1470,7 @@ module Gitlab
expect { Gitlab::Ci::YamlProcessor.new(config) }
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
- 'rspec: unknown key in `extends`')
+ 'rspec: unknown keys in `extends` (something)')
end
end
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index b7924302014..51e5bdc6307 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -150,13 +150,13 @@ describe Gitlab::ContributionsCalendar do
end
describe '#starting_year' do
- it "should be the start of last year" do
+ it "is the start of last year" do
expect(calendar.starting_year).to eq(last_year.year)
end
end
describe '#starting_month' do
- it "should be the start of this month" do
+ it "is the start of this month" do
expect(calendar.starting_month).to eq(today.month)
end
end
diff --git a/spec/lib/gitlab/correlation_id_spec.rb b/spec/lib/gitlab/correlation_id_spec.rb
deleted file mode 100644
index 584d1f48386..00000000000
--- a/spec/lib/gitlab/correlation_id_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::CorrelationId do
- describe '.use_id' do
- it 'yields when executed' do
- expect { |blk| described_class.use_id('id', &blk) }.to yield_control
- end
-
- it 'stacks correlation ids' do
- described_class.use_id('id1') do
- described_class.use_id('id2') do |current_id|
- expect(current_id).to eq('id2')
- end
- end
- end
-
- it 'for missing correlation id it generates random one' do
- described_class.use_id('id1') do
- described_class.use_id(nil) do |current_id|
- expect(current_id).not_to be_empty
- expect(current_id).not_to eq('id1')
- end
- end
- end
- end
-
- describe '.current_id' do
- subject { described_class.current_id }
-
- it 'returns last correlation id' do
- described_class.use_id('id1') do
- described_class.use_id('id2') do
- is_expected.to eq('id2')
- end
- end
- end
- end
-
- describe '.current_or_new_id' do
- subject { described_class.current_or_new_id }
-
- context 'when correlation id is set' do
- it 'returns last correlation id' do
- described_class.use_id('id1') do
- is_expected.to eq('id1')
- end
- end
- end
-
- context 'when correlation id is missing' do
- it 'returns a new correlation id' do
- expect(described_class).to receive(:new_id)
- .and_call_original
-
- is_expected.not_to be_empty
- end
- end
- end
-
- describe '.ids' do
- subject { described_class.send(:ids) }
-
- it 'returns empty list if not correlation is used' do
- is_expected.to be_empty
- end
-
- it 'returns list if correlation ids are used' do
- described_class.use_id('id1') do
- described_class.use_id('id2') do
- is_expected.to eq(%w(id1 id2))
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index caf9fc5442c..909dbffa38f 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -115,9 +115,8 @@ describe Gitlab::CurrentSettings do
shared_examples 'a non-persisted ApplicationSetting object' do
let(:current_settings) { described_class.current_application_settings }
- it 'returns a non-persisted ApplicationSetting object' do
- expect(current_settings).to be_a(ApplicationSetting)
- expect(current_settings).not_to be_persisted
+ it 'returns a FakeApplicationSettings object' do
+ expect(current_settings).to be_a(Gitlab::FakeApplicationSettings)
end
it 'uses the default value from ApplicationSetting.defaults' do
@@ -143,9 +142,19 @@ describe Gitlab::CurrentSettings do
it_behaves_like 'a non-persisted ApplicationSetting object'
- it 'uses the value from the DB attribute if present and not overriden by an accessor' do
+ it 'uses the value from the DB attribute if present and not overridden by an accessor' do
expect(current_settings.home_page_url).to eq(db_settings.home_page_url)
end
+
+ context 'when a new column is used before being migrated' do
+ before do
+ allow(ApplicationSetting).to receive(:defaults).and_return({ foo: 'bar' })
+ end
+
+ it 'uses the default value if present' do
+ expect(current_settings.foo).to eq('bar')
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb
index 00cb1e6446a..f7642182a17 100644
--- a/spec/lib/gitlab/danger/helper_spec.rb
+++ b/spec/lib/gitlab/danger/helper_spec.rb
@@ -2,7 +2,6 @@
require 'fast_spec_helper'
require 'rspec-parameterized'
-require 'webmock/rspec'
require 'gitlab/danger/helper'
@@ -19,39 +18,6 @@ describe Gitlab::Danger::Helper do
end
end
- let(:teammate_json) do
- <<~JSON
- [
- {
- "username": "in-gitlab-ce",
- "name": "CE maintainer",
- "projects":{ "gitlab-ce": "maintainer backend" }
- },
- {
- "username": "in-gitlab-ee",
- "name": "EE reviewer",
- "projects":{ "gitlab-ee": "reviewer frontend" }
- }
- ]
- JSON
- end
-
- let(:ce_teammate_matcher) do
- satisfy do |teammate|
- teammate.username == 'in-gitlab-ce' &&
- teammate.name == 'CE maintainer' &&
- teammate.projects == { 'gitlab-ce' => 'maintainer backend' }
- end
- end
-
- let(:ee_teammate_matcher) do
- satisfy do |teammate|
- teammate.username == 'in-gitlab-ee' &&
- teammate.name == 'EE reviewer' &&
- teammate.projects == { 'gitlab-ee' => 'reviewer frontend' }
- end
- end
-
let(:fake_git) { double('fake-git') }
subject(:helper) { FakeDanger.new(git: fake_git) }
@@ -119,69 +85,6 @@ describe Gitlab::Danger::Helper do
end
end
- describe '#team' do
- subject(:team) { helper.team }
-
- context 'HTTP failure' do
- before do
- WebMock
- .stub_request(:get, 'https://about.gitlab.com/roulette.json')
- .to_return(status: 404)
- end
-
- it 'raises a pretty error' do
- expect { team }.to raise_error(/Failed to read/)
- end
- end
-
- context 'JSON failure' do
- before do
- WebMock
- .stub_request(:get, 'https://about.gitlab.com/roulette.json')
- .to_return(body: 'INVALID JSON')
- end
-
- it 'raises a pretty error' do
- expect { team }.to raise_error(/Failed to parse/)
- end
- end
-
- context 'success' do
- before do
- WebMock
- .stub_request(:get, 'https://about.gitlab.com/roulette.json')
- .to_return(body: teammate_json)
- end
-
- it 'returns an array of teammates' do
- is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher)
- end
-
- it 'memoizes the result' do
- expect(team.object_id).to eq(helper.team.object_id)
- end
- end
- end
-
- describe '#project_team' do
- subject { helper.project_team }
-
- before do
- WebMock
- .stub_request(:get, 'https://about.gitlab.com/roulette.json')
- .to_return(body: teammate_json)
- end
-
- it 'filters team by project_name' do
- expect(helper)
- .to receive(:project_name)
- .at_least(:once)
- .and_return('gitlab-ce')
-
- is_expected.to contain_exactly(ce_teammate_matcher)
- end
- end
-
describe '#changes_by_category' do
it 'categorizes changed files' do
expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo qa/foo ee/changelogs/foo.yml] }
@@ -191,9 +94,8 @@ describe Gitlab::Danger::Helper do
expect(helper.changes_by_category).to eq(
backend: %w[foo.rb],
database: %w[db/foo],
- docs: %w[foo.md],
frontend: %w[foo.js],
- none: %w[ee/changelogs/foo.yml],
+ none: %w[ee/changelogs/foo.yml foo.md],
qa: %w[qa/foo],
unknown: %w[foo]
)
@@ -202,13 +104,13 @@ describe Gitlab::Danger::Helper do
describe '#category_for_file' do
where(:path, :expected_category) do
- 'doc/foo' | :docs
- 'CONTRIBUTING.md' | :docs
- 'LICENSE' | :docs
- 'MAINTENANCE.md' | :docs
- 'PHILOSOPHY.md' | :docs
- 'PROCESS.md' | :docs
- 'README.md' | :docs
+ 'doc/foo' | :none
+ 'CONTRIBUTING.md' | :none
+ 'LICENSE' | :none
+ 'MAINTENANCE.md' | :none
+ 'PHILOSOPHY.md' | :none
+ 'PROCESS.md' | :none
+ 'README.md' | :none
'ee/doc/foo' | :unknown
'ee/README' | :unknown
@@ -265,14 +167,15 @@ describe Gitlab::Danger::Helper do
'changelogs/foo' | :none
'ee/changelogs/foo' | :none
+ 'locale/gitlab.pot' | :none
'FOO' | :unknown
'foo' | :unknown
'foo/bar.rb' | :backend
'foo/bar.js' | :frontend
- 'foo/bar.txt' | :docs
- 'foo/bar.md' | :docs
+ 'foo/bar.txt' | :none
+ 'foo/bar.md' | :none
end
with_them do
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
new file mode 100644
index 00000000000..40dce0c5378
--- /dev/null
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'webmock/rspec'
+
+require 'gitlab/danger/roulette'
+
+describe Gitlab::Danger::Roulette do
+ let(:teammate_json) do
+ <<~JSON
+ [
+ {
+ "username": "in-gitlab-ce",
+ "name": "CE maintainer",
+ "projects":{ "gitlab-ce": "maintainer backend" }
+ },
+ {
+ "username": "in-gitlab-ee",
+ "name": "EE reviewer",
+ "projects":{ "gitlab-ee": "reviewer frontend" }
+ }
+ ]
+ JSON
+ end
+
+ let(:ce_teammate_matcher) do
+ satisfy do |teammate|
+ teammate.username == 'in-gitlab-ce' &&
+ teammate.name == 'CE maintainer' &&
+ teammate.projects == { 'gitlab-ce' => 'maintainer backend' }
+ end
+ end
+
+ let(:ee_teammate_matcher) do
+ satisfy do |teammate|
+ teammate.username == 'in-gitlab-ee' &&
+ teammate.name == 'EE reviewer' &&
+ teammate.projects == { 'gitlab-ee' => 'reviewer frontend' }
+ end
+ end
+
+ subject(:roulette) { Object.new.extend(described_class) }
+
+ describe '#team' do
+ subject(:team) { roulette.team }
+
+ context 'HTTP failure' do
+ before do
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(status: 404)
+ end
+
+ it 'raises a pretty error' do
+ expect { team }.to raise_error(/Failed to read/)
+ end
+ end
+
+ context 'JSON failure' do
+ before do
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(body: 'INVALID JSON')
+ end
+
+ it 'raises a pretty error' do
+ expect { team }.to raise_error(/Failed to parse/)
+ end
+ end
+
+ context 'success' do
+ before do
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(body: teammate_json)
+ end
+
+ it 'returns an array of teammates' do
+ is_expected.to contain_exactly(ce_teammate_matcher, ee_teammate_matcher)
+ end
+
+ it 'memoizes the result' do
+ expect(team.object_id).to eq(roulette.team.object_id)
+ end
+ end
+ end
+
+ describe '#project_team' do
+ subject { roulette.project_team('gitlab-ce') }
+
+ before do
+ WebMock
+ .stub_request(:get, described_class::ROULETTE_DATA_URL)
+ .to_return(body: teammate_json)
+ end
+
+ it 'filters team by project_name' do
+ is_expected.to contain_exactly(ce_teammate_matcher)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/danger/teammate_spec.rb b/spec/lib/gitlab/danger/teammate_spec.rb
new file mode 100644
index 00000000000..6a6cf1429c8
--- /dev/null
+++ b/spec/lib/gitlab/danger/teammate_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+require 'gitlab/danger/teammate'
+
+describe Gitlab::Danger::Teammate do
+ subject { described_class.new(options) }
+ let(:options) { { 'projects' => projects, 'role' => role } }
+ let(:projects) { { project => capabilities } }
+ let(:role) { 'Engineer, Manage' }
+ let(:labels) { [] }
+ let(:project) { double }
+
+ context 'when having multiple capabilities' do
+ let(:capabilities) { ['reviewer backend', 'maintainer frontend', 'trainee_maintainer qa'] }
+
+ it '#reviewer? supports multiple roles per project' do
+ expect(subject.reviewer?(project, :backend, labels)).to be_truthy
+ end
+
+ it '#traintainer? supports multiple roles per project' do
+ expect(subject.traintainer?(project, :qa, labels)).to be_truthy
+ end
+
+ it '#maintainer? supports multiple roles per project' do
+ expect(subject.maintainer?(project, :frontend, labels)).to be_truthy
+ end
+
+ context 'when labels contain Create and the category is test' do
+ let(:labels) { ['Create'] }
+
+ context 'when role is Test Automation Engineer, Create' do
+ let(:role) { 'Test Automation Engineer, Create' }
+
+ it '#reviewer? returns true' do
+ expect(subject.reviewer?(project, :test, labels)).to be_truthy
+ end
+
+ it '#maintainer? returns false' do
+ expect(subject.maintainer?(project, :test, labels)).to be_falsey
+ end
+ end
+
+ context 'when role is Test Automation Engineer, Manage' do
+ let(:role) { 'Test Automation Engineer, Manage' }
+
+ it '#reviewer? returns false' do
+ expect(subject.reviewer?(project, :test, labels)).to be_falsey
+ end
+ end
+ end
+ end
+
+ context 'when having single capability' do
+ let(:capabilities) { 'reviewer backend' }
+
+ it '#reviewer? supports one role per project' do
+ expect(subject.reviewer?(project, :backend, labels)).to be_truthy
+ end
+
+ it '#traintainer? supports one role per project' do
+ expect(subject.traintainer?(project, :database, labels)).to be_falsey
+ end
+
+ it '#maintainer? supports one role per project' do
+ expect(subject.maintainer?(project, :frontend, labels)).to be_falsey
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/deployment_spec.rb b/spec/lib/gitlab/data_builder/deployment_spec.rb
new file mode 100644
index 00000000000..0a6e2302b09
--- /dev/null
+++ b/spec/lib/gitlab/data_builder/deployment_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::DataBuilder::Deployment do
+ describe '.build' do
+ it 'returns the object kind for a deployment' do
+ deployment = build(:deployment)
+
+ data = described_class.build(deployment)
+
+ expect(data[:object_kind]).to eq('deployment')
+ end
+
+ it 'returns data for the given build' do
+ environment = create(:environment, name: "somewhere")
+ project = create(:project, :repository, name: 'myproj')
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :failed, environment: environment, sha: commit.sha, project: project)
+ deployable = deployment.deployable
+ expected_deployable_url = Gitlab::Routing.url_helpers.project_job_url(deployable.project, deployable)
+ expected_user_url = Gitlab::Routing.url_helpers.user_url(deployment.user)
+ expected_commit_url = Gitlab::UrlBuilder.build(commit)
+
+ data = described_class.build(deployment)
+
+ expect(data[:status]).to eq('failed')
+ expect(data[:deployable_id]).to eq(deployable.id)
+ expect(data[:deployable_url]).to eq(expected_deployable_url)
+ expect(data[:environment]).to eq("somewhere")
+ expect(data[:project]).to eq(project.hook_attrs)
+ expect(data[:short_sha]).to eq(deployment.short_sha)
+ expect(data[:user]).to eq(deployment.user.hook_attrs)
+ expect(data[:user_url]).to eq(expected_user_url)
+ expect(data[:commit_url]).to eq(expected_commit_url)
+ expect(data[:commit_title]).to eq(commit.title)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/pipeline_spec.rb b/spec/lib/gitlab/data_builder/pipeline_spec.rb
index 9ef987a0826..1f36fd5c6ef 100644
--- a/spec/lib/gitlab/data_builder/pipeline_spec.rb
+++ b/spec/lib/gitlab/data_builder/pipeline_spec.rb
@@ -50,5 +50,14 @@ describe Gitlab::DataBuilder::Pipeline do
it { expect(attributes[:variables]).to be_a(Array) }
it { expect(attributes[:variables]).to contain_exactly({ key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1' }) }
end
+
+ context 'when pipeline is a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.all_pipelines.first }
+
+ it 'returns a source ref' do
+ expect(attributes[:ref]).to eq(merge_request.source_branch)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb
index 0c4decc6518..46ad674a1eb 100644
--- a/spec/lib/gitlab/data_builder/push_spec.rb
+++ b/spec/lib/gitlab/data_builder/push_spec.rb
@@ -23,9 +23,12 @@ describe Gitlab::DataBuilder::Push do
describe '.build' do
let(:data) do
- described_class.build(project, user, Gitlab::Git::BLANK_SHA,
- '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
- 'refs/tags/v1.1.0')
+ described_class.build(
+ project: project,
+ user: user,
+ oldrev: Gitlab::Git::BLANK_SHA,
+ newrev: '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b',
+ ref: 'refs/tags/v1.1.0')
end
it { expect(data).to be_a(Hash) }
@@ -47,7 +50,7 @@ describe Gitlab::DataBuilder::Push do
include_examples 'deprecated repository hook data'
it 'does not raise an error when given nil commits' do
- expect { described_class.build(spy, spy, spy, spy, 'refs/tags/v1.1.0', nil) }
+ expect { described_class.build(project: spy, user: spy, ref: 'refs/tags/v1.1.0', commits: nil) }
.not_to raise_error
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 b44e8c5a110..bd3c66d0548 100644
--- a/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/reltuples_count_strategy_spec.rb
@@ -6,10 +6,11 @@ describe Gitlab::Database::Count::ReltuplesCountStrategy do
create(:identity)
end
- let(:models) { [Project, Identity] }
subject { described_class.new(models).count }
describe '#count', :postgresql do
+ let(:models) { [Project, Identity] }
+
context 'when reltuples is up to date' do
before do
ActiveRecord::Base.connection.execute('ANALYZE projects')
@@ -23,6 +24,22 @@ describe Gitlab::Database::Count::ReltuplesCountStrategy do
end
end
+ context 'when models using single-type inheritance are used' do
+ let(:models) { [Group, CiService, Namespace] }
+
+ before do
+ models.each do |model|
+ ActiveRecord::Base.connection.execute("ANALYZE #{model.table_name}")
+ end
+ end
+
+ it 'returns nil counts for inherited tables' do
+ models.each { |model| expect(model).not_to receive(:count) }
+
+ expect(subject).to eq({ Namespace => 3 })
+ end
+ end
+
context 'insufficient permissions' do
it 'returns an empty hash' do
allow(ActiveRecord::Base).to receive(:transaction).and_raise(PG::InsufficientPrivilege)
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 203f9344a41..40d810b195b 100644
--- a/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
+++ b/spec/lib/gitlab/database/count/tablesample_count_strategy_spec.rb
@@ -4,15 +4,23 @@ describe Gitlab::Database::Count::TablesampleCountStrategy do
before do
create_list(:project, 3)
create(:identity)
+ create(:group)
end
- let(:models) { [Project, Identity] }
+ let(:models) { [Project, Identity, Group, Namespace] }
let(:strategy) { described_class.new(models) }
subject { strategy.count }
describe '#count', :postgresql do
- let(:estimates) { { Project => threshold + 1, Identity => threshold - 1 } }
+ let(:estimates) do
+ {
+ Project => threshold + 1,
+ Identity => threshold - 1,
+ Group => threshold + 1,
+ Namespace => threshold + 1
+ }
+ end
let(:threshold) { Gitlab::Database::Count::TablesampleCountStrategy::EXACT_COUNT_THRESHOLD }
before do
@@ -30,9 +38,13 @@ describe Gitlab::Database::Count::TablesampleCountStrategy do
context 'for tables with an estimated large size' do
it 'performs a tablesample count' do
expect(Project).not_to receive(:count)
+ expect(Group).not_to receive(:count)
+ expect(Namespace).not_to receive(:count)
result = subject
expect(result[Project]).to eq(3)
+ expect(result[Group]).to eq(1)
+ expect(result[Namespace]).to eq(4)
end
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
index 248cca25a2c..81419e51635 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete
Project.find(project.id)
end
- describe "#remove_last_ocurrence" do
+ describe "#remove_last_occurrence" do
it "removes only the last occurrence of a string" do
input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace"
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
index 1d31f96159c..ddd54a669a3 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do
describe '#rename_wildcard_paths' do
it_behaves_like 'renames child namespaces'
- it 'should rename projects' do
+ it 'renames projects' do
rename_projects = double
expect(described_class::RenameProjects)
.to receive(:new).with(['the-path'], subject)
@@ -40,7 +40,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do
end
describe '#rename_root_paths' do
- it 'should rename namespaces' do
+ it 'renames namespaces' do
rename_namespaces = double
expect(described_class::RenameNamespaces)
.to receive(:new).with(['the-path'], subject)
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 60106ee3c0b..5f57cd6b825 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -17,6 +17,20 @@ describe Gitlab::Database do
end
end
+ describe '.human_adapter_name' do
+ it 'returns PostgreSQL when using PostgreSQL' do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+
+ expect(described_class.human_adapter_name).to eq('PostgreSQL')
+ end
+
+ it 'returns MySQL when using MySQL' do
+ allow(described_class).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.human_adapter_name).to eq('MySQL')
+ end
+ end
+
# These are just simple smoke tests to check if the methods work (regardless
# of what they may return).
describe '.mysql?' do
@@ -87,6 +101,38 @@ describe Gitlab::Database do
end
end
+ describe '.postgresql_minimum_supported_version?' do
+ it 'returns false when not using PostgreSQL' do
+ allow(described_class).to receive(:postgresql?).and_return(false)
+
+ expect(described_class.postgresql_minimum_supported_version?).to eq(false)
+ end
+
+ context 'when using PostgreSQL' do
+ before do
+ allow(described_class).to receive(:postgresql?).and_return(true)
+ end
+
+ it 'returns false when using PostgreSQL 9.5' do
+ allow(described_class).to receive(:version).and_return('9.5')
+
+ expect(described_class.postgresql_minimum_supported_version?).to eq(false)
+ end
+
+ it 'returns true when using PostgreSQL 9.6' do
+ allow(described_class).to receive(:version).and_return('9.6')
+
+ expect(described_class.postgresql_minimum_supported_version?).to eq(true)
+ end
+
+ it 'returns true when using PostgreSQL 10 or newer' do
+ allow(described_class).to receive(:version).and_return('10')
+
+ expect(described_class.postgresql_minimum_supported_version?).to eq(true)
+ end
+ end
+ end
+
describe '.join_lateral_supported?' do
it 'returns false when using MySQL' do
allow(described_class).to receive(:postgresql?).and_return(false)
@@ -195,6 +241,12 @@ describe Gitlab::Database do
end
end
+ describe '.pg_last_xact_replay_timestamp' do
+ it 'returns pg_last_xact_replay_timestamp' do
+ expect(described_class.pg_last_xact_replay_timestamp).to eq('pg_last_xact_replay_timestamp')
+ end
+ end
+
describe '.nulls_last_order' do
context 'when using PostgreSQL' do
before do
diff --git a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
index 4d222564fd0..0ebd8994636 100644
--- a/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/composer_json_linker_spec.rb
@@ -50,8 +50,8 @@ describe Gitlab::DependencyLinker::ComposerJsonLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
- it 'links the module name' do
- expect(subject).to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel'))
+ it 'does not link the module name' do
+ expect(subject).not_to include(link('laravel/laravel', 'https://packagist.org/packages/laravel/laravel'))
end
it 'links the homepage' do
diff --git a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
index a97803b119e..f00f6b47b94 100644
--- a/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemfile_linker_spec.rb
@@ -41,13 +41,16 @@ describe Gitlab::DependencyLinker::GemfileLinker do
end
it 'links dependencies' do
- expect(subject).to include(link('rails', 'https://rubygems.org/gems/rails'))
expect(subject).to include(link('rails-deprecated_sanitizer', 'https://rubygems.org/gems/rails-deprecated_sanitizer'))
- expect(subject).to include(link('responders', 'https://rubygems.org/gems/responders'))
- expect(subject).to include(link('sprockets', 'https://rubygems.org/gems/sprockets'))
expect(subject).to include(link('default_value_for', 'https://rubygems.org/gems/default_value_for'))
end
+ it 'links to external dependencies' do
+ expect(subject).to include(link('rails', 'https://github.com/rails/rails'))
+ expect(subject).to include(link('responders', 'https://github.com/rails/responders'))
+ expect(subject).to include(link('sprockets', 'https://gitlab.example.com/gems/sprockets'))
+ end
+
it 'links GitHub repos' do
expect(subject).to include(link('rails/rails', 'https://github.com/rails/rails'))
expect(subject).to include(link('rails/responders', 'https://github.com/rails/responders'))
diff --git a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
index 24ad7d12f4c..6c6a5d70576 100644
--- a/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/gemspec_linker_spec.rb
@@ -43,8 +43,8 @@ describe Gitlab::DependencyLinker::GemspecLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
- it 'links the gem name' do
- expect(subject).to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git'))
+ it 'does not link the gem name' do
+ expect(subject).not_to include(link('gitlab_git', 'https://rubygems.org/gems/gitlab_git'))
end
it 'links the license' do
diff --git a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
index 1e8b72afb7b..9050127af7f 100644
--- a/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/package_json_linker_spec.rb
@@ -33,7 +33,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
"express": "4.2.x",
"bigpipe": "bigpipe/pagelet",
"plates": "https://github.com/flatiron/plates/tarball/master",
- "karma": "^1.4.1"
+ "karma": "^1.4.1",
+ "random": "git+https://EdOverflow@github.com/example/example.git"
},
"devDependencies": {
"vows": "^0.7.0",
@@ -51,8 +52,8 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
- it 'links the module name' do
- expect(subject).to include(link('module-name', 'https://npmjs.com/package/module-name'))
+ it 'does not link the module name' do
+ expect(subject).not_to include(link('module-name', 'https://npmjs.com/package/module-name'))
end
it 'links the homepage' do
@@ -71,14 +72,21 @@ describe Gitlab::DependencyLinker::PackageJsonLinker do
expect(subject).to include(link('primus', 'https://npmjs.com/package/primus'))
expect(subject).to include(link('async', 'https://npmjs.com/package/async'))
expect(subject).to include(link('express', 'https://npmjs.com/package/express'))
- expect(subject).to include(link('bigpipe', 'https://npmjs.com/package/bigpipe'))
- expect(subject).to include(link('plates', 'https://npmjs.com/package/plates'))
expect(subject).to include(link('karma', 'https://npmjs.com/package/karma'))
expect(subject).to include(link('vows', 'https://npmjs.com/package/vows'))
expect(subject).to include(link('assume', 'https://npmjs.com/package/assume'))
expect(subject).to include(link('pre-commit', 'https://npmjs.com/package/pre-commit'))
end
+ it 'links dependencies to URL detected on value' do
+ expect(subject).to include(link('bigpipe', 'https://github.com/bigpipe/pagelet'))
+ expect(subject).to include(link('plates', 'https://github.com/flatiron/plates/tarball/master'))
+ end
+
+ it 'does not link to NPM when invalid git URL' do
+ expect(subject).not_to include(link('random', 'https://npmjs.com/package/random'))
+ end
+
it 'links GitHub repos' do
expect(subject).to include(link('bigpipe/pagelet', 'https://github.com/bigpipe/pagelet'))
end
diff --git a/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb
new file mode 100644
index 00000000000..9bfb1b13a2b
--- /dev/null
+++ b/spec/lib/gitlab/dependency_linker/parser/gemfile_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::DependencyLinker::Parser::Gemfile do
+ describe '#parse' do
+ let(:file_content) do
+ <<-CONTENT.strip_heredoc
+ source 'https://rubygems.org'
+
+ gem "rails", '4.2.6', github: "rails/rails"
+ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
+ gem 'responders', '~> 2.0', :github => 'rails/responders'
+ gem 'sprockets', '~> 3.6.0', git: 'https://gitlab.example.com/gems/sprockets'
+ gem 'default_value_for', '~> 3.0.0'
+ CONTENT
+ end
+
+ subject { described_class.new(file_content).parse(keyword: 'gem') }
+
+ def fetch_package(name)
+ subject.find { |package| package.name == name }
+ end
+
+ it 'returns parsed packages' do
+ expect(subject.size).to eq(5)
+ expect(subject).to all(be_a(Gitlab::DependencyLinker::Package))
+ end
+
+ it 'packages respond to name and external_ref accordingly' do
+ expect(fetch_package('rails')).to have_attributes(name: 'rails',
+ github_ref: 'rails/rails',
+ git_ref: nil)
+
+ expect(fetch_package('sprockets')).to have_attributes(name: 'sprockets',
+ github_ref: nil,
+ git_ref: 'https://gitlab.example.com/gems/sprockets')
+
+ expect(fetch_package('default_value_for')).to have_attributes(name: 'default_value_for',
+ github_ref: nil,
+ git_ref: nil)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
index cdfd7ad9826..8f1b523653e 100644
--- a/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podfile_linker_spec.rb
@@ -43,7 +43,10 @@ describe Gitlab::DependencyLinker::PodfileLinker do
it 'links packages' do
expect(subject).to include(link('AFNetworking', 'https://cocoapods.org/pods/AFNetworking'))
- expect(subject).to include(link('Interstellar/Core', 'https://cocoapods.org/pods/Interstellar'))
+ end
+
+ it 'links external packages' do
+ expect(subject).to include(link('Interstellar/Core', 'https://github.com/ashfurrow/Interstellar.git'))
end
it 'links Git repos' do
diff --git a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
index ed60ab45955..bacec830103 100644
--- a/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
+++ b/spec/lib/gitlab/dependency_linker/podspec_linker_spec.rb
@@ -42,8 +42,8 @@ describe Gitlab::DependencyLinker::PodspecLinker do
%{<a href="#{url}" rel="nofollow noreferrer noopener" target="_blank">#{name}</a>}
end
- it 'links the gem name' do
- expect(subject).to include(link('Reachability', 'https://cocoapods.org/pods/Reachability'))
+ it 'does not link the pod name' do
+ expect(subject).not_to include(link('Reachability', 'https://cocoapods.org/pods/Reachability'))
end
it 'links the license' do
diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
index 256166dbad3..0697594c725 100644
--- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
+++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb
@@ -27,7 +27,7 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do
let(:diffable) { merge_request.merge_request_diff }
end
- it 'it uses a different cache key if diff line keys change' do
+ it 'uses a different cache key if diff line keys change' do
mr_diff = described_class.new(merge_request.merge_request_diff, diff_options: nil)
key = mr_diff.cache_key
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 862590268ca..cc36060f864 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -8,6 +8,47 @@ describe Gitlab::Diff::File do
let(:diff) { commit.raw_diffs.first }
let(:diff_file) { described_class.new(diff, diff_refs: commit.diff_refs, repository: project.repository) }
+ def create_file(file_name, content)
+ Files::CreateService.new(
+ project,
+ project.owner,
+ commit_message: 'Update',
+ start_branch: branch_name,
+ branch_name: branch_name,
+ file_path: file_name,
+ file_content: content
+ ).execute
+
+ project.commit(branch_name).diffs.diff_files.first
+ end
+
+ def update_file(file_name, content)
+ Files::UpdateService.new(
+ project,
+ project.owner,
+ commit_message: 'Update',
+ start_branch: branch_name,
+ branch_name: branch_name,
+ file_path: file_name,
+ file_content: content
+ ).execute
+
+ project.commit(branch_name).diffs.diff_files.first
+ end
+
+ def delete_file(file_name)
+ Files::DeleteService.new(
+ project,
+ project.owner,
+ commit_message: 'Update',
+ start_branch: branch_name,
+ branch_name: branch_name,
+ file_path: file_name
+ ).execute
+
+ project.commit(branch_name).diffs.diff_files.first
+ end
+
describe '#diff_lines' do
let(:diff_lines) { diff_file.diff_lines }
@@ -31,6 +72,13 @@ describe Gitlab::Diff::File do
expect(diff_file.diff_lines_for_serializer.last.type).to eq('match')
end
+ context 'when called multiple times' do
+ it 'only adds bottom match line once' do
+ expect(diff_file.diff_lines_for_serializer.size).to eq(31)
+ expect(diff_file.diff_lines_for_serializer.size).to eq(31)
+ end
+ end
+
context 'when deleted' do
let(:commit) { project.commit('d59c60028b053793cecfb4022de34602e1a9218e') }
let(:diff_file) { commit.diffs.diff_file_with_old_path('files/js/commit.js.coffee') }
@@ -675,47 +723,6 @@ describe Gitlab::Diff::File do
end
let(:branch_name) { 'master' }
- def create_file(file_name, content)
- Files::CreateService.new(
- project,
- project.owner,
- commit_message: 'Update',
- start_branch: branch_name,
- branch_name: branch_name,
- file_path: file_name,
- file_content: content
- ).execute
-
- project.commit(branch_name).diffs.diff_files.first
- end
-
- def update_file(file_name, content)
- Files::UpdateService.new(
- project,
- project.owner,
- commit_message: 'Update',
- start_branch: branch_name,
- branch_name: branch_name,
- file_path: file_name,
- file_content: content
- ).execute
-
- project.commit(branch_name).diffs.diff_files.first
- end
-
- def delete_file(file_name)
- Files::DeleteService.new(
- project,
- project.owner,
- commit_message: 'Update',
- start_branch: branch_name,
- branch_name: branch_name,
- file_path: file_name
- ).execute
-
- project.commit(branch_name).diffs.diff_files.first
- end
-
context 'when empty file is created' do
it 'returns true' do
diff_file = create_file('empty.md', '')
@@ -751,4 +758,123 @@ describe Gitlab::Diff::File do
end
end
end
+
+ describe '#fully_expanded?' do
+ let(:project) do
+ create(:project, :custom_repo, files: {})
+ end
+ let(:branch_name) { 'master' }
+
+ context 'when empty file is created' do
+ it 'returns true' do
+ diff_file = create_file('empty.md', '')
+
+ expect(diff_file.fully_expanded?).to be_truthy
+ end
+ end
+
+ context 'when empty file is deleted' do
+ it 'returns true' do
+ create_file('empty.md', '')
+ diff_file = delete_file('empty.md')
+
+ expect(diff_file.fully_expanded?).to be_truthy
+ end
+ end
+
+ context 'when short file with last line removed' do
+ it 'returns true' do
+ create_file('with-content.md', (1..3).to_a.join("\n"))
+ diff_file = update_file('with-content.md', (1..2).to_a.join("\n"))
+
+ expect(diff_file.fully_expanded?).to be_truthy
+ end
+ end
+
+ context 'when a single line is added to empty file' do
+ it 'returns true' do
+ create_file('empty.md', '')
+ diff_file = update_file('empty.md', 'new content')
+
+ expect(diff_file.fully_expanded?).to be_truthy
+ end
+ end
+
+ context 'when single line file is changed' do
+ it 'returns true' do
+ create_file('file.md', 'foo')
+ diff_file = update_file('file.md', 'bar')
+
+ expect(diff_file.fully_expanded?).to be_truthy
+ end
+ end
+
+ context 'when long file is changed' do
+ before do
+ create_file('file.md', (1..999).to_a.join("\n"))
+ end
+
+ context 'when first line is removed' do
+ it 'returns true' do
+ diff_file = update_file('file.md', (2..999).to_a.join("\n"))
+
+ expect(diff_file.fully_expanded?).to be_falsey
+ end
+ end
+
+ context 'when last line is removed' do
+ it 'returns true' do
+ diff_file = update_file('file.md', (1..998).to_a.join("\n"))
+
+ expect(diff_file.fully_expanded?).to be_falsey
+ end
+ end
+
+ context 'when first and last lines are removed' do
+ it 'returns false' do
+ diff_file = update_file('file.md', (2..998).to_a.join("\n"))
+
+ expect(diff_file.fully_expanded?).to be_falsey
+ end
+ end
+
+ context 'when first and last lines are changed' do
+ it 'returns false' do
+ content = (2..998).to_a
+ content.prepend('a')
+ content.append('z')
+ content = content.join("\n")
+
+ diff_file = update_file('file.md', content)
+
+ expect(diff_file.fully_expanded?).to be_falsey
+ end
+ end
+
+ context 'when every line are changed' do
+ it 'returns true' do
+ diff_file = update_file('file.md', "hi\n" * 999)
+
+ expect(diff_file.fully_expanded?).to be_truthy
+ end
+ end
+
+ context 'when all contents are cleared' do
+ it 'returns true' do
+ diff_file = update_file('file.md', "")
+
+ expect(diff_file.fully_expanded?).to be_truthy
+ end
+ end
+
+ context 'when file is binary' do
+ it 'returns true' do
+ diff_file = update_file('file.md', (1..998).to_a.join("\n"))
+ allow(diff_file).to receive(:binary?).and_return(true)
+
+ expect(diff_file.fully_expanded?).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/suggestion_diff_spec.rb b/spec/lib/gitlab/diff/suggestion_diff_spec.rb
new file mode 100644
index 00000000000..5a32c2bea37
--- /dev/null
+++ b/spec/lib/gitlab/diff/suggestion_diff_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::SuggestionDiff do
+ describe '#diff_lines' do
+ let(:from_content) do
+ <<-BLOB.strip_heredoc
+ "tags": ["devel", "development", "nightly"],
+ "desktop-file-name-prefix": "(Development) ",
+ "finish-args": "foo",
+ BLOB
+ end
+
+ let(:to_content) do
+ <<-BLOB.strip_heredoc
+ "buildsystem": "meson",
+ "builddir": true,
+ "name": "nautilus",
+ "bar": "bar",
+ BLOB
+ end
+
+ let(:suggestion) do
+ instance_double(Suggestion, from_line: 12,
+ from_content: from_content,
+ to_content: to_content)
+ end
+
+ subject { described_class.new(suggestion).diff_lines }
+
+ let(:expected_diff_lines) do
+ [
+ { old_pos: 12, new_pos: 12, type: "match", text: "@@ -12 +12" },
+ { old_pos: 12, new_pos: 12, type: "old", text: "-\"tags\": [\"devel\", \"development\", \"nightly\"]," },
+ { old_pos: 13, new_pos: 12, type: "old", text: "-\"desktop-file-name-prefix\": \"(Development) \"," },
+ { old_pos: 14, new_pos: 12, type: "old", text: "-\"finish-args\": \"foo\"," },
+ { old_pos: 15, new_pos: 12, type: "new", text: "+\"buildsystem\": \"meson\"," },
+ { old_pos: 15, new_pos: 13, type: "new", text: "+\"builddir\": true," },
+ { old_pos: 15, new_pos: 14, type: "new", text: "+\"name\": \"nautilus\"," },
+ { old_pos: 15, new_pos: 15, type: "new", text: "+\"bar\": \"bar\"," }
+ ]
+ end
+
+ it 'returns diff lines with correct line numbers' do
+ diff_lines = subject
+
+ expect(diff_lines).to all(be_a(Gitlab::Diff::Line))
+
+ expected_diff_lines.each_with_index do |expected_line, index|
+ expect(diff_lines[index].to_hash).to include(expected_line)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/suggestion_spec.rb b/spec/lib/gitlab/diff/suggestion_spec.rb
new file mode 100644
index 00000000000..d7ca0e0a522
--- /dev/null
+++ b/spec/lib/gitlab/diff/suggestion_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::Suggestion do
+ shared_examples 'correct suggestion raw content' do
+ it 'returns correct raw data' do
+ expect(suggestion.to_hash).to include(from_content: expected_lines.join,
+ to_content: "#{text}\n",
+ lines_above: above,
+ lines_below: below)
+ end
+
+ it 'returns diff lines with correct line numbers' do
+ diff_lines = suggestion.diff_lines
+
+ expect(diff_lines).to all(be_a(Gitlab::Diff::Line))
+
+ expected_diff_lines.each_with_index do |expected_line, index|
+ expect(diff_lines[index].to_hash).to include(expected_line)
+ end
+ end
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:position) do
+ Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs)
+ end
+ let(:diff_file) do
+ position.diff_file(project.repository)
+ end
+ let(:text) { "# parsed suggestion content\n# with comments" }
+
+ def blob_lines_data(from_line, to_line)
+ diff_file.new_blob_lines_between(from_line, to_line)
+ end
+
+ def blob_data
+ blob = diff_file.new_blob
+ blob.load_all_data!
+ blob.data
+ end
+
+ let(:suggestion) do
+ described_class.new(text, line: line, above: above, below: below, diff_file: diff_file)
+ end
+
+ describe '#to_hash' do
+ context 'when changing content surpasses the top limit' do
+ let(:line) { 4 }
+ let(:above) { 5 }
+ let(:below) { 2 }
+ let(:expected_above) { line - 1 }
+ let(:expected_below) { below }
+ let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
+ let(:expected_diff_lines) do
+ [
+ { old_pos: 1, new_pos: 1, type: 'old', text: "-require 'fileutils'" },
+ { old_pos: 2, new_pos: 1, type: 'old', text: "-require 'open3'" },
+ { old_pos: 3, new_pos: 1, type: 'old', text: "-" },
+ { old_pos: 4, new_pos: 1, type: 'old', text: "-module Popen" },
+ { old_pos: 5, new_pos: 1, type: 'old', text: "- extend self" },
+ { old_pos: 6, new_pos: 1, type: 'old', text: "-" },
+ { old_pos: 7, new_pos: 1, type: 'new', text: "+# parsed suggestion content" },
+ { old_pos: 7, new_pos: 2, type: 'new', text: "+# with comments" }
+ ]
+ end
+
+ it_behaves_like 'correct suggestion raw content'
+ end
+
+ context 'when changing content surpasses the amount of lines in the blob (bottom)' do
+ let(:line) { 5 }
+ let(:above) { 1 }
+ let(:below) { blob_data.lines.size + 10 }
+ let(:expected_below) { below }
+ let(:expected_above) { above }
+ let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
+ let(:expected_diff_lines) do
+ [
+ { old_pos: 4, new_pos: 4, type: "match", text: "@@ -4 +4" },
+ { old_pos: 4, new_pos: 4, type: "old", text: "-module Popen" },
+ { old_pos: 5, new_pos: 4, type: "old", text: "- extend self" },
+ { old_pos: 6, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 7, new_pos: 4, type: "old", text: "- def popen(cmd, path=nil)" },
+ { old_pos: 8, new_pos: 4, type: "old", text: "- unless cmd.is_a?(Array)" },
+ { old_pos: 9, new_pos: 4, type: "old", text: "- raise RuntimeError, \"System commands must be given as an array of strings\"" },
+ { old_pos: 10, new_pos: 4, type: "old", text: "- end" },
+ { old_pos: 11, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 12, new_pos: 4, type: "old", text: "- path ||= Dir.pwd" },
+ { old_pos: 13, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 14, new_pos: 4, type: "old", text: "- vars = {" },
+ { old_pos: 15, new_pos: 4, type: "old", text: "- \"PWD\" => path" },
+ { old_pos: 16, new_pos: 4, type: "old", text: "- }" },
+ { old_pos: 17, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 18, new_pos: 4, type: "old", text: "- options = {" },
+ { old_pos: 19, new_pos: 4, type: "old", text: "- chdir: path" },
+ { old_pos: 20, new_pos: 4, type: "old", text: "- }" },
+ { old_pos: 21, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 22, new_pos: 4, type: "old", text: "- unless File.directory?(path)" },
+ { old_pos: 23, new_pos: 4, type: "old", text: "- FileUtils.mkdir_p(path)" },
+ { old_pos: 24, new_pos: 4, type: "old", text: "- end" },
+ { old_pos: 25, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 26, new_pos: 4, type: "old", text: "- @cmd_output = \"\"" },
+ { old_pos: 27, new_pos: 4, type: "old", text: "- @cmd_status = 0" },
+ { old_pos: 28, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 29, new_pos: 4, type: "old", text: "- Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|" },
+ { old_pos: 30, new_pos: 4, type: "old", text: "- @cmd_output << stdout.read" },
+ { old_pos: 31, new_pos: 4, type: "old", text: "- @cmd_output << stderr.read" },
+ { old_pos: 32, new_pos: 4, type: "old", text: "- @cmd_status = wait_thr.value.exitstatus" },
+ { old_pos: 33, new_pos: 4, type: "old", text: "- end" },
+ { old_pos: 34, new_pos: 4, type: "old", text: "-" },
+ { old_pos: 35, new_pos: 4, type: "old", text: "- return @cmd_output, @cmd_status" },
+ { old_pos: 36, new_pos: 4, type: "old", text: "- end" },
+ { old_pos: 37, new_pos: 4, type: "old", text: "-end" },
+ { old_pos: 38, new_pos: 4, type: "new", text: "+# parsed suggestion content" },
+ { old_pos: 38, new_pos: 5, type: "new", text: "+# with comments" }
+ ]
+ end
+
+ it_behaves_like 'correct suggestion raw content'
+ end
+
+ context 'when lines are within blob lines boundary' do
+ let(:line) { 5 }
+ let(:above) { 2 }
+ let(:below) { 3 }
+ let(:expected_below) { below }
+ let(:expected_above) { above }
+ let(:expected_lines) { blob_lines_data(line - expected_above, line + expected_below) }
+ let(:expected_diff_lines) do
+ [
+ { old_pos: 3, new_pos: 3, type: "match", text: "@@ -3 +3" },
+ { old_pos: 3, new_pos: 3, type: "old", text: "-" },
+ { old_pos: 4, new_pos: 3, type: "old", text: "-module Popen" },
+ { old_pos: 5, new_pos: 3, type: "old", text: "- extend self" },
+ { old_pos: 6, new_pos: 3, type: "old", text: "-" },
+ { old_pos: 7, new_pos: 3, type: "old", text: "- def popen(cmd, path=nil)" },
+ { old_pos: 8, new_pos: 3, type: "old", text: "- unless cmd.is_a?(Array)" },
+ { old_pos: 9, new_pos: 3, type: "new", text: "+# parsed suggestion content" },
+ { old_pos: 9, new_pos: 4, type: "new", text: "+# with comments" }
+ ]
+ end
+
+ it_behaves_like 'correct suggestion raw content'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/diff/suggestions_parser_spec.rb b/spec/lib/gitlab/diff/suggestions_parser_spec.rb
new file mode 100644
index 00000000000..1f2af42f6e7
--- /dev/null
+++ b/spec/lib/gitlab/diff/suggestions_parser_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::SuggestionsParser do
+ describe '.parse' do
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+ let(:position) do
+ Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs)
+ end
+
+ let(:diff_file) do
+ position.diff_file(project.repository)
+ end
+
+ subject do
+ described_class.parse(markdown, project: merge_request.project,
+ position: position)
+ end
+
+ def blob_lines_data(from_line, to_line)
+ diff_file.new_blob_lines_between(from_line, to_line).join
+ end
+
+ context 'single-line suggestions' do
+ let(:markdown) do
+ <<-MARKDOWN.strip_heredoc
+ ```suggestion
+ foo
+ bar
+ ```
+
+ ```
+ nothing
+ ```
+
+ ```suggestion
+ xpto
+ baz
+ ```
+
+ ```thing
+ this is not a suggestion, it's a thing
+ ```
+ MARKDOWN
+ end
+
+ it 'returns a list of Gitlab::Diff::Suggestion' do
+ expect(subject).to all(be_a(Gitlab::Diff::Suggestion))
+ expect(subject.size).to eq(2)
+ end
+
+ it 'parsed suggestion has correct data' do
+ from_line, to_line = position.new_line, position.new_line
+
+ expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
+ to_content: " foo\n bar\n",
+ lines_above: 0,
+ lines_below: 0)
+
+ expect(subject.second.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
+ to_content: " xpto\n baz\n",
+ lines_above: 0,
+ lines_below: 0)
+ end
+ end
+
+ context 'multi-line suggestions' do
+ let(:markdown) do
+ <<-MARKDOWN.strip_heredoc
+ ```suggestion:-2+1
+ # above and below
+ ```
+
+ ```
+ nothing
+ ```
+
+ ```suggestion:-3
+ # only above
+ ```
+
+ ```suggestion:+3
+ # only below
+ ```
+
+ ```thing
+ this is not a suggestion, it's a thing
+ ```
+ MARKDOWN
+ end
+
+ it 'returns a list of Gitlab::Diff::Suggestion' do
+ expect(subject).to all(be_a(Gitlab::Diff::Suggestion))
+ expect(subject.size).to eq(3)
+ end
+
+ it 'suggestion with above and below param has correct data' do
+ from_line = position.new_line - 2
+ to_line = position.new_line + 1
+
+ expect(subject.first.to_hash).to include(from_content: blob_lines_data(from_line, to_line),
+ to_content: " # above and below\n",
+ lines_above: 2,
+ lines_below: 1)
+ end
+
+ it 'suggestion with above param has correct data' do
+ from_line = position.new_line - 3
+ to_line = position.new_line
+
+ expect(subject.second.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
+ to_content: " # only above\n",
+ lines_above: 3,
+ lines_below: 0)
+ end
+
+ it 'suggestion with below param has correct data' do
+ from_line = position.new_line
+ to_line = position.new_line + 3
+
+ expect(subject.third.to_hash).to eq(from_content: blob_lines_data(from_line, to_line),
+ to_content: " # only below\n",
+ lines_above: 0,
+ lines_below: 3)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
index fe26ebb8796..15ee8c40b55 100644
--- a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
+++ b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb
@@ -3,31 +3,32 @@
require 'spec_helper'
describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do
+ def fake_file(offset)
+ {
+ text: 'foo',
+ type: 'new',
+ index: 2 + offset,
+ old_pos: 10 + offset,
+ new_pos: 11 + offset,
+ line_code: 'xpto',
+ rich_text: '<blips>blops</blips>'
+ }
+ end
+
+ let(:mapping) do
+ {
+ 3 => [
+ fake_file(0),
+ fake_file(1)
+ ],
+ 4 => [
+ fake_file(2)
+ ]
+ }
+ end
+
describe '#write_multiple' do
it 'sets multiple keys serializing content as JSON' do
- mapping = {
- 3 => [
- {
- text: 'foo',
- type: 'new',
- index: 2,
- old_pos: 10,
- new_pos: 11,
- line_code: 'xpto',
- rich_text: '<blips>blops</blips>'
- },
- {
- text: 'foo',
- type: 'new',
- index: 3,
- old_pos: 11,
- new_pos: 12,
- line_code: 'xpto',
- rich_text: '<blops>blips</blops>'
- }
- ]
- }
-
described_class.write_multiple(mapping)
mapping.each do |key, value|
@@ -41,53 +42,16 @@ describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do
describe '#read_multiple' do
it 'reads multiple keys and serializes content into Gitlab::Diff::Line objects' do
- mapping = {
- 3 => [
- {
- text: 'foo',
- type: 'new',
- index: 2,
- old_pos: 11,
- new_pos: 12,
- line_code: 'xpto',
- rich_text: '<blips>blops</blips>'
- },
- {
- text: 'foo',
- type: 'new',
- index: 3,
- old_pos: 10,
- new_pos: 11,
- line_code: 'xpto',
- rich_text: '<blips>blops</blips>'
- }
- ]
- }
-
described_class.write_multiple(mapping)
found = described_class.read_multiple(mapping.keys)
- expect(found.size).to eq(1)
+ expect(found.size).to eq(2)
expect(found.first.size).to eq(2)
expect(found.first).to all(be_a(Gitlab::Diff::Line))
end
it 'returns nil when cached key is not found' do
- mapping = {
- 3 => [
- {
- text: 'foo',
- type: 'new',
- index: 2,
- old_pos: 11,
- new_pos: 12,
- line_code: 'xpto',
- rich_text: '<blips>blops</blips>'
- }
- ]
- }
-
described_class.write_multiple(mapping)
found = described_class.read_multiple([2, 3])
@@ -95,8 +59,30 @@ describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cache do
expect(found.size).to eq(2)
expect(found.first).to eq(nil)
- expect(found.second.size).to eq(1)
+ expect(found.second.size).to eq(2)
expect(found.second).to all(be_a(Gitlab::Diff::Line))
end
end
+
+ describe '#clear_multiple' do
+ it 'removes all named keys' do
+ described_class.write_multiple(mapping)
+
+ described_class.clear_multiple(mapping.keys)
+
+ expect(described_class.read_multiple(mapping.keys)).to all(be_nil)
+ end
+
+ it 'only removed named keys' do
+ to_clear, to_leave = mapping.keys
+
+ described_class.write_multiple(mapping)
+ described_class.clear_multiple([to_clear])
+
+ cleared, left = described_class.read_multiple([to_clear, to_leave])
+
+ expect(cleared).to be_nil
+ expect(left).to all(be_a(Gitlab::Diff::Line))
+ end
+ end
end
diff --git a/spec/lib/gitlab/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 429816efec3..88ea98eb1e1 100644
--- a/spec/lib/gitlab/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -189,14 +189,23 @@ describe Gitlab::EncodingHelper do
end
end
- describe '#binary_stringio' do
+ describe '#binary_io' do
it 'does not mutate the original string encoding' do
test = 'my-test'
- io_stream = ext_class.binary_stringio(test)
+ io_stream = ext_class.binary_io(test)
expect(io_stream.external_encoding.name).to eq('ASCII-8BIT')
expect(test.encoding.name).to eq('UTF-8')
end
+
+ it 'returns a copy of the IO with the correct encoding' do
+ test = fixture_file_upload('spec/fixtures/doc_sample.txt').to_io
+
+ io_stream = ext_class.binary_io(test)
+
+ expect(io_stream.external_encoding.name).to eq('ASCII-8BIT')
+ expect(test).not_to eq(io_stream)
+ end
end
end
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
index f69cb502ca6..a7cb0bb2a87 100644
--- a/spec/lib/gitlab/etag_caching/router_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -19,6 +19,24 @@ describe Gitlab::EtagCaching::Router do
expect(result.name).to eq 'issue_title'
end
+ it 'matches with a project name that includes a suffix of create' do
+ result = described_class.match(
+ '/group/test-create/issues/123/realtime_changes'
+ )
+
+ expect(result).to be_present
+ expect(result.name).to eq 'issue_title'
+ end
+
+ it 'matches with a project name that includes a prefix of create' do
+ result = described_class.match(
+ '/group/create-test/issues/123/realtime_changes'
+ )
+
+ expect(result).to be_present
+ expect(result.name).to eq 'issue_title'
+ end
+
it 'matches project pipelines endpoint' do
result = described_class.match(
'/my-group/my-project/pipelines.json'
diff --git a/spec/lib/gitlab/external_authorization/access_spec.rb b/spec/lib/gitlab/external_authorization/access_spec.rb
new file mode 100644
index 00000000000..5dc2521b310
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/access_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Access, :clean_gitlab_redis_cache do
+ subject(:access) { described_class.new(build(:user), 'dummy_label') }
+
+ describe '#loaded?' do
+ it 'is `true` when it was loaded recently' do
+ Timecop.freeze do
+ allow(access).to receive(:loaded_at).and_return(5.minutes.ago)
+
+ expect(access).to be_loaded
+ end
+ end
+
+ it 'is `false` when there is no loading time' do
+ expect(access).not_to be_loaded
+ end
+
+ it 'is `false` when there the result was loaded a long time ago' do
+ Timecop.freeze do
+ allow(access).to receive(:loaded_at).and_return(2.weeks.ago)
+
+ expect(access).not_to be_loaded
+ end
+ end
+ end
+
+ describe 'load!' do
+ let(:fake_client) { double('ExternalAuthorization::Client') }
+ let(:fake_response) do
+ double(
+ 'Response',
+ 'successful?' => true,
+ 'valid?' => true,
+ 'reason' => nil
+ )
+ end
+
+ before do
+ allow(access).to receive(:load_from_cache)
+ allow(fake_client).to receive(:request_access).and_return(fake_response)
+ allow(Gitlab::ExternalAuthorization::Client).to receive(:new) { fake_client }
+ end
+
+ context 'when loading from the webservice' do
+ it 'loads from the webservice it the cache was empty' do
+ expect(access).to receive(:load_from_cache)
+ expect(access).to receive(:load_from_service).and_call_original
+
+ access.load!
+
+ expect(access).to be_loaded
+ end
+
+ it 'assigns the accessibility, reason and loaded_at' do
+ allow(fake_response).to receive(:successful?).and_return(false)
+ allow(fake_response).to receive(:reason).and_return('Inaccessible label')
+
+ access.load!
+
+ expect(access.reason).to eq('Inaccessible label')
+ expect(access).not_to have_access
+ expect(access.loaded_at).not_to be_nil
+ end
+
+ it 'returns itself' do
+ expect(access.load!).to eq(access)
+ end
+
+ it 'stores the result in redis' do
+ Timecop.freeze do
+ fake_cache = double
+ expect(fake_cache).to receive(:store).with(true, nil, Time.now)
+ expect(access).to receive(:cache).and_return(fake_cache)
+
+ access.load!
+ end
+ end
+
+ context 'when the request fails' do
+ before do
+ allow(fake_client).to receive(:request_access) do
+ raise ::Gitlab::ExternalAuthorization::RequestFailed.new('Service unavailable')
+ end
+ end
+
+ it 'is loaded' do
+ access.load!
+
+ expect(access).to be_loaded
+ end
+
+ it 'assigns the correct accessibility, reason and loaded_at' do
+ access.load!
+
+ expect(access.reason).to eq('Service unavailable')
+ expect(access).not_to have_access
+ expect(access.loaded_at).not_to be_nil
+ end
+
+ it 'does not store the result in redis' do
+ fake_cache = double
+ expect(fake_cache).not_to receive(:store)
+ allow(access).to receive(:cache).and_return(fake_cache)
+
+ access.load!
+ end
+ end
+ end
+
+ context 'When loading from cache' do
+ let(:fake_cache) { double('ExternalAuthorization::Cache') }
+
+ before do
+ allow(access).to receive(:cache).and_return(fake_cache)
+ end
+
+ it 'does not load from the webservice' do
+ Timecop.freeze do
+ expect(fake_cache).to receive(:load).and_return([true, nil, Time.now])
+
+ expect(access).to receive(:load_from_cache).and_call_original
+ expect(access).not_to receive(:load_from_service)
+
+ access.load!
+ end
+ end
+
+ it 'loads from the webservice when the cached result was too old' do
+ Timecop.freeze do
+ expect(fake_cache).to receive(:load).and_return([true, nil, 2.days.ago])
+
+ expect(access).to receive(:load_from_cache).and_call_original
+ expect(access).to receive(:load_from_service).and_call_original
+ allow(fake_cache).to receive(:store)
+
+ access.load!
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization/cache_spec.rb b/spec/lib/gitlab/external_authorization/cache_spec.rb
new file mode 100644
index 00000000000..58e7d626707
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/cache_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Cache, :clean_gitlab_redis_cache do
+ let(:user) { build_stubbed(:user) }
+ let(:cache_key) { "external_authorization:user-#{user.id}:label-dummy_label" }
+
+ subject(:cache) { described_class.new(user, 'dummy_label') }
+
+ def read_from_redis(key)
+ Gitlab::Redis::Cache.with do |redis|
+ redis.hget(cache_key, key)
+ end
+ end
+
+ def set_in_redis(key, value)
+ Gitlab::Redis::Cache.with do |redis|
+ redis.hmset(cache_key, key, value)
+ end
+ end
+
+ describe '#load' do
+ it 'reads stored info from redis' do
+ Timecop.freeze do
+ set_in_redis(:access, false)
+ set_in_redis(:reason, 'Access denied for now')
+ set_in_redis(:refreshed_at, Time.now)
+
+ access, reason, refreshed_at = cache.load
+
+ expect(access).to eq(false)
+ expect(reason).to eq('Access denied for now')
+ expect(refreshed_at).to be_within(1.second).of(Time.now)
+ end
+ end
+ end
+
+ describe '#store' do
+ it 'sets the values in redis' do
+ Timecop.freeze do
+ cache.store(true, 'the reason', Time.now)
+
+ expect(read_from_redis(:access)).to eq('true')
+ expect(read_from_redis(:reason)).to eq('the reason')
+ expect(read_from_redis(:refreshed_at)).to eq(Time.now.to_s)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization/client_spec.rb b/spec/lib/gitlab/external_authorization/client_spec.rb
new file mode 100644
index 00000000000..fa18c1e56e8
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/client_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Client do
+ let(:user) { build(:user, email: 'dummy_user@example.com') }
+ let(:dummy_url) { 'https://dummy.net/' }
+ subject(:client) { described_class.new(user, 'dummy_label') }
+
+ before do
+ stub_application_setting(external_authorization_service_url: dummy_url)
+ end
+
+ describe '#request_access' do
+ it 'performs requests to the configured endpoint' do
+ expect(Excon).to receive(:post).with(dummy_url, any_args)
+
+ client.request_access
+ end
+
+ it 'adds the correct params for the user to the body of the request' do
+ expected_body = {
+ user_identifier: 'dummy_user@example.com',
+ project_classification_label: 'dummy_label'
+ }.to_json
+ expect(Excon).to receive(:post)
+ .with(dummy_url, hash_including(body: expected_body))
+
+ client.request_access
+ end
+
+ it 'respects the the timeout' do
+ stub_application_setting(
+ external_authorization_service_timeout: 3
+ )
+
+ expect(Excon).to receive(:post).with(dummy_url,
+ hash_including(
+ connect_timeout: 3,
+ read_timeout: 3,
+ write_timeout: 3
+ ))
+
+ client.request_access
+ end
+
+ it 'adds the mutual tls params when they are present' do
+ stub_application_setting(
+ external_auth_client_cert: 'the certificate data',
+ external_auth_client_key: 'the key data',
+ external_auth_client_key_pass: 'open sesame'
+ )
+ expected_params = {
+ client_cert_data: 'the certificate data',
+ client_key_data: 'the key data',
+ client_key_pass: 'open sesame'
+ }
+
+ expect(Excon).to receive(:post).with(dummy_url, hash_including(expected_params))
+
+ client.request_access
+ end
+
+ it 'returns an expected response' do
+ expect(Excon).to receive(:post)
+
+ expect(client.request_access)
+ .to be_kind_of(::Gitlab::ExternalAuthorization::Response)
+ end
+
+ it 'wraps exceptions if the request fails' do
+ expect(Excon).to receive(:post) { raise Excon::Error.new('the request broke') }
+
+ expect { client.request_access }
+ .to raise_error(::Gitlab::ExternalAuthorization::RequestFailed)
+ end
+
+ describe 'for ldap users' do
+ let(:user) do
+ create(:omniauth_user,
+ email: 'dummy_user@example.com',
+ extern_uid: 'external id',
+ provider: 'ldapprovider')
+ end
+
+ it 'includes the ldap dn for ldap users' do
+ expected_body = {
+ user_identifier: 'dummy_user@example.com',
+ project_classification_label: 'dummy_label',
+ user_ldap_dn: 'external id'
+ }.to_json
+ expect(Excon).to receive(:post)
+ .with(dummy_url, hash_including(body: expected_body))
+
+ client.request_access
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization/logger_spec.rb b/spec/lib/gitlab/external_authorization/logger_spec.rb
new file mode 100644
index 00000000000..81f1b2390e6
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/logger_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Logger do
+ let(:request_time) { Time.parse('2018-03-26 20:22:15') }
+
+ def fake_access(has_access, user, load_type = :request)
+ access = double('access')
+ allow(access).to receive_messages(user: user,
+ has_access?: has_access,
+ loaded_at: request_time,
+ label: 'dummy_label',
+ load_type: load_type)
+
+ access
+ end
+
+ describe '.log_access' do
+ it 'logs a nice message for an access request' do
+ expected_message = "GRANTED admin@example.com access to 'dummy_label' (the/project/path)"
+ fake_access = fake_access(true, build(:user, email: 'admin@example.com'))
+
+ expect(described_class).to receive(:info).with(expected_message)
+
+ described_class.log_access(fake_access, 'the/project/path')
+ end
+
+ it 'does not trip without a project path' do
+ expected_message = "DENIED admin@example.com access to 'dummy_label'"
+ fake_access = fake_access(false, build(:user, email: 'admin@example.com'))
+
+ expect(described_class).to receive(:info).with(expected_message)
+
+ described_class.log_access(fake_access, nil)
+ end
+
+ it 'adds the load time for cached accesses' do
+ expected_message = "DENIED admin@example.com access to 'dummy_label' - cache #{request_time}"
+ fake_access = fake_access(false, build(:user, email: 'admin@example.com'), :cache)
+
+ expect(described_class).to receive(:info).with(expected_message)
+
+ described_class.log_access(fake_access, nil)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization/response_spec.rb b/spec/lib/gitlab/external_authorization/response_spec.rb
new file mode 100644
index 00000000000..43211043eca
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization/response_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization::Response do
+ let(:excon_response) { double }
+ subject(:response) { described_class.new(excon_response) }
+
+ describe '#valid?' do
+ it 'is valid for 200, 401, and 403 responses' do
+ [200, 401, 403].each do |status|
+ allow(excon_response).to receive(:status).and_return(status)
+
+ expect(response).to be_valid
+ end
+ end
+
+ it "is invalid for other statuses" do
+ expect(excon_response).to receive(:status).and_return(500)
+
+ expect(response).not_to be_valid
+ end
+ end
+
+ describe '#reason' do
+ it 'returns a reason if it was included in the response body' do
+ expect(excon_response).to receive(:body).and_return({ reason: 'Not authorized' }.to_json)
+
+ expect(response.reason).to eq('Not authorized')
+ end
+
+ it 'returns nil when there was no body' do
+ expect(excon_response).to receive(:body).and_return('')
+
+ expect(response.reason).to eq(nil)
+ end
+ end
+
+ describe '#successful?' do
+ it 'is `true` if the status is 200' do
+ allow(excon_response).to receive(:status).and_return(200)
+
+ expect(response).to be_successful
+ end
+
+ it 'is `false` if the status is 401 or 403' do
+ [401, 403].each do |status|
+ allow(excon_response).to receive(:status).and_return(status)
+
+ expect(response).not_to be_successful
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/external_authorization_spec.rb b/spec/lib/gitlab/external_authorization_spec.rb
new file mode 100644
index 00000000000..7394fbfe0ce
--- /dev/null
+++ b/spec/lib/gitlab/external_authorization_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::ExternalAuthorization, :request_store do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:user) { build(:user) }
+ let(:label) { 'dummy_label' }
+
+ describe '#access_allowed?' do
+ it 'is always true when the feature is disabled' do
+ # Not using `stub_application_setting` because the method is prepended in
+ # `EE::ApplicationSetting` which breaks when using `any_instance`
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/33587
+ expect(::Gitlab::CurrentSettings.current_application_settings)
+ .to receive(:external_authorization_service_enabled) { false }
+
+ expect(described_class).not_to receive(:access_for_user_to_label)
+
+ expect(described_class.access_allowed?(user, label)).to be_truthy
+ end
+ end
+
+ describe '#rejection_reason' do
+ it 'is always nil when the feature is disabled' do
+ expect(::Gitlab::CurrentSettings.current_application_settings)
+ .to receive(:external_authorization_service_enabled) { false }
+
+ expect(described_class).not_to receive(:access_for_user_to_label)
+
+ expect(described_class.rejection_reason(user, label)).to be_nil
+ end
+ end
+
+ describe '#access_for_user_to_label' do
+ it 'only loads the access once per request' do
+ enable_external_authorization_service_check
+
+ expect(::Gitlab::ExternalAuthorization::Access)
+ .to receive(:new).with(user, label).once.and_call_original
+
+ 2.times { described_class.access_for_user_to_label(user, label, nil) }
+ end
+
+ it 'logs the access request once per request' do
+ expect(::Gitlab::ExternalAuthorization::Logger)
+ .to receive(:log_access)
+ .with(an_instance_of(::Gitlab::ExternalAuthorization::Access),
+ 'the/project/path')
+ .once
+
+ 2.times { described_class.access_for_user_to_label(user, label, 'the/project/path') }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/fake_application_settings_spec.rb b/spec/lib/gitlab/fake_application_settings_spec.rb
index af12e13d36d..c81cb83d9f4 100644
--- a/spec/lib/gitlab/fake_application_settings_spec.rb
+++ b/spec/lib/gitlab/fake_application_settings_spec.rb
@@ -1,32 +1,33 @@
require 'spec_helper'
describe Gitlab::FakeApplicationSettings do
- let(:defaults) { { password_authentication_enabled_for_web: false, foobar: 'asdf', signup_enabled: true, 'test?' => 123 } }
+ let(:defaults) do
+ described_class.defaults.merge(
+ foobar: 'asdf',
+ 'test?' => 123
+ )
+ end
- subject { described_class.new(defaults) }
+ let(:setting) { described_class.new(defaults) }
it 'wraps OpenStruct variables properly' do
- expect(subject.password_authentication_enabled_for_web).to be_falsey
- expect(subject.signup_enabled).to be_truthy
- expect(subject.foobar).to eq('asdf')
+ expect(setting.password_authentication_enabled_for_web).to be_truthy
+ expect(setting.signup_enabled).to be_truthy
+ expect(setting.foobar).to eq('asdf')
end
it 'defines predicate methods' do
- expect(subject.password_authentication_enabled_for_web?).to be_falsey
- expect(subject.signup_enabled?).to be_truthy
- end
-
- it 'predicate method changes when value is updated' do
- subject.password_authentication_enabled_for_web = true
-
- expect(subject.password_authentication_enabled_for_web?).to be_truthy
+ expect(setting.password_authentication_enabled_for_web?).to be_truthy
+ expect(setting.signup_enabled?).to be_truthy
end
it 'does not define a predicate method' do
- expect(subject.foobar?).to be_nil
+ expect(setting.foobar?).to be_nil
end
it 'does not override an existing predicate method' do
- expect(subject.test?).to eq(123)
+ expect(setting.test?).to eq(123)
end
+
+ it_behaves_like 'application settings examples'
end
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 49a423191bb..dce56bbd2c4 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
expect(described_class.main).to match_asset_path '/assets/favicon.png'
end
- it 'has blue favicon for development' do
+ it 'has blue favicon for development', unless: Gitlab.ee? do
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
expect(described_class.main).to match_asset_path '/assets/favicon-blue.png'
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index a1b5cea88c0..1c24244c3a6 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Git::Blob, :seed_helper do
end
end
- describe '.find' do
+ shared_examples '.find' do
context 'nil path' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, nil) }
@@ -128,6 +128,20 @@ describe Gitlab::Git::Blob, :seed_helper do
end
end
+ describe '.find with Gitaly enabled' do
+ it_behaves_like '.find'
+ end
+
+ describe '.find with Rugged enabled', :enable_rugged do
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
+
+ described_class.find(repository, SeedRepo::Commit::ID, 'files/images/6049019_460s.jpg')
+ end
+
+ it_behaves_like '.find'
+ end
+
describe '.raw' do
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
let(:bad_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::BigCommit::ID) }
@@ -327,7 +341,7 @@ describe Gitlab::Git::Blob, :seed_helper do
it { expect(blob.mode).to eq("100755") }
end
- context 'file with Chinese text' do
+ context 'file with Japanese text' do
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/テスト.txt") }
it { expect(blob.name).to eq("テスト.txt") }
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 2611ebed25b..25052a79916 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -112,14 +112,14 @@ describe Gitlab::Git::Commit, :seed_helper do
end
context 'Class methods' do
- describe '.find' do
- it "should return first head commit if without params" do
+ shared_examples '.find' do
+ it "returns first head commit if without params" do
expect(described_class.last(repository).id).to eq(
rugged_repo.head.target.oid
)
end
- it "should return valid commit" do
+ it "returns valid commit" do
expect(described_class.find(repository, SeedRepo::Commit::ID)).to be_valid_commit
end
@@ -127,21 +127,21 @@ describe Gitlab::Git::Commit, :seed_helper do
expect(described_class.find(repository, SeedRepo::Commit::ID).parent_ids).to be_an(Array)
end
- it "should return valid commit for tag" do
+ it "returns valid commit for tag" do
expect(described_class.find(repository, 'v1.0.0').id).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
end
- it "should return nil for non-commit ids" do
+ it "returns nil for non-commit ids" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
expect(described_class.find(repository, blob.id)).to be_nil
end
- it "should return nil for parent of non-commit object" do
+ it "returns nil for parent of non-commit object" do
blob = Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "files/ruby/popen.rb")
expect(described_class.find(repository, "#{blob.id}^")).to be_nil
end
- it "should return nil for nonexisting ids" do
+ it "returns nil for nonexisting ids" do
expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil
end
@@ -154,6 +154,20 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
+ describe '.find with Gitaly enabled' do
+ it_should_behave_like '.find'
+ end
+
+ describe '.find with Rugged enabled', :enable_rugged do
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
+
+ described_class.find(repository, SeedRepo::Commit::ID)
+ end
+
+ it_should_behave_like '.find'
+ end
+
describe '.last_for_path' do
context 'no path' do
subject { described_class.last_for_path(repository, 'master') }
@@ -314,7 +328,7 @@ describe Gitlab::Git::Commit, :seed_helper do
end
describe '.find_all' do
- it 'should return a return a collection of commits' do
+ it 'returns a return a collection of commits' do
commits = described_class.find_all(repository)
expect(commits).to all( be_a_kind_of(described_class) )
@@ -366,7 +380,32 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
- describe '#batch_by_oid' do
+ shared_examples '.batch_by_oid' do
+ context 'with multiple OIDs' do
+ let(:oids) { [SeedRepo::Commit::ID, SeedRepo::FirstCommit::ID] }
+
+ it 'returns multiple commits' do
+ commits = described_class.batch_by_oid(repository, oids)
+
+ expect(commits.count).to eq(2)
+ expect(commits).to all( be_a(Gitlab::Git::Commit) )
+ expect(commits.first.sha).to eq(SeedRepo::Commit::ID)
+ expect(commits.second.sha).to eq(SeedRepo::FirstCommit::ID)
+ end
+ end
+
+ context 'when oids is empty' do
+ it 'returns empty commits' do
+ commits = described_class.batch_by_oid(repository, [])
+
+ expect(commits.count).to eq(0)
+ end
+ end
+ end
+
+ describe '.batch_by_oid with Gitaly enabled' do
+ it_should_behave_like '.batch_by_oid'
+
context 'when oids is empty' do
it 'makes no Gitaly request' do
expect(Gitlab::GitalyClient).not_to receive(:call)
@@ -376,6 +415,16 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
+ describe '.batch_by_oid with Rugged enabled', :enable_rugged do
+ it_should_behave_like '.batch_by_oid'
+
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged).to receive(:rev_parse).with(SeedRepo::Commit::ID).and_call_original
+
+ described_class.batch_by_oid(repository, [SeedRepo::Commit::ID])
+ end
+ end
+
shared_examples 'extracting commit signature' do
context 'when the commit is signed' do
let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
@@ -523,6 +572,18 @@ describe Gitlab::Git::Commit, :seed_helper do
end
end
+ describe '#gitaly_commit?' do
+ context 'when the commit data comes from gitaly' do
+ it { expect(commit.gitaly_commit?).to eq(true) }
+ end
+
+ context 'when the commit data comes from a Hash' do
+ let(:commit) { described_class.new(repository, sample_commit_hash) }
+
+ it { expect(commit.gitaly_commit?).to eq(false) }
+ end
+ end
+
describe '#has_zero_stats?' do
it { expect(commit.has_zero_stats?).to eq(false) }
end
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 1d22329b670..9ab669ad488 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -182,7 +182,7 @@ EOT
context "without default options" do
let(:filtered_options) { described_class.filter_diff_options(options) }
- it "should filter invalid options" do
+ it "filters invalid options" do
expect(filtered_options).not_to have_key(:invalid_opt)
end
end
@@ -193,16 +193,16 @@ EOT
described_class.filter_diff_options(options, default_options)
end
- it "should filter invalid options" do
+ it "filters invalid options" do
expect(filtered_options).not_to have_key(:invalid_opt)
expect(filtered_options).not_to have_key(:bad_opt)
end
- it "should merge with default options" do
+ it "merges with default options" do
expect(filtered_options).to have_key(:ignore_whitespace_change)
end
- it "should override default options" do
+ it "overrides default options" do
expect(filtered_options).to have_key(:max_files)
expect(filtered_options[:max_files]).to eq(100)
end
diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
index 6fd2b33486b..de81dcd227d 100644
--- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb
+++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Git::GitmodulesParser do
- it 'should parse a .gitmodules file correctly' do
+ it 'parses a .gitmodules file correctly' do
data = <<~GITMODULES
[submodule "vendor/libgit2"]
path = vendor/libgit2
diff --git a/spec/lib/gitlab/git/object_pool_spec.rb b/spec/lib/gitlab/git/object_pool_spec.rb
index 0d5069568e1..ebeb7b7b633 100644
--- a/spec/lib/gitlab/git/object_pool_spec.rb
+++ b/spec/lib/gitlab/git/object_pool_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
describe Gitlab::Git::ObjectPool do
+ include RepoHelpers
+
let(:pool_repository) { create(:pool_repository) }
let(:source_repository) { pool_repository.source_project.repository }
@@ -76,4 +78,43 @@ describe Gitlab::Git::ObjectPool do
end
end
end
+
+ describe '#fetch' do
+ let(:source_repository_path) { File.join(TestEnv.repos_path, source_repository.relative_path) }
+ let(:source_repository_rugged) { Rugged::Repository.new(source_repository_path) }
+ let(:commit_count) { source_repository.commit_count }
+
+ context "when the object's pool repository exists" do
+ it 'does not raise an error' do
+ expect { subject.fetch }.not_to raise_error
+ end
+ end
+
+ context "when the object's pool repository does not exist" do
+ before do
+ subject.delete
+ end
+
+ it "re-creates the object pool's repository" do
+ subject.fetch
+
+ expect(subject.repository.exists?).to be true
+ end
+
+ it 'does not raise an error' do
+ expect { subject.fetch }.not_to raise_error
+ end
+
+ it 'fetches objects from the source repository' do
+ new_commit_id = new_commit_edit_old_file(source_repository_rugged).oid
+
+ expect(subject.repository.exists?).to be false
+
+ subject.fetch
+
+ expect(subject.repository.commit_count('refs/remotes/origin/master')).to eq(commit_count)
+ expect(subject.repository.commit(new_commit_id).id).to eq(new_commit_id)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/pre_receive_error_spec.rb b/spec/lib/gitlab/git/pre_receive_error_spec.rb
index 1b8be62dec6..cb030e38032 100644
--- a/spec/lib/gitlab/git/pre_receive_error_spec.rb
+++ b/spec/lib/gitlab/git/pre_receive_error_spec.rb
@@ -1,9 +1,19 @@
require 'spec_helper'
describe Gitlab::Git::PreReceiveError do
- it 'makes its message HTML-friendly' do
- ex = described_class.new("hello\nworld\n")
+ Gitlab::Git::PreReceiveError::SAFE_MESSAGE_PREFIXES.each do |prefix|
+ context "error messages prefixed with #{prefix}" do
+ it 'accepts only errors lines with the prefix' do
+ ex = described_class.new("#{prefix} Hello,\nworld!")
- expect(ex.message).to eq('hello<br>world<br>')
+ expect(ex.message).to eq('Hello,')
+ end
+
+ it 'makes its message HTML-friendly' do
+ ex = described_class.new("#{prefix} Hello,\n#{prefix} world!\n")
+
+ expect(ex.message).to eq('Hello,<br>world!')
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/git/repository_cleaner_spec.rb b/spec/lib/gitlab/git/repository_cleaner_spec.rb
index 7f9cc2bc9ec..7bba0107e58 100644
--- a/spec/lib/gitlab/git/repository_cleaner_spec.rb
+++ b/spec/lib/gitlab/git/repository_cleaner_spec.rb
@@ -6,57 +6,62 @@ describe Gitlab::Git::RepositoryCleaner do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository }
let(:head_sha) { repository.head_commit.id }
- let(:object_map_data) { "#{head_sha} #{'0' * 40}" }
+ let(:object_map_data) { "#{head_sha} #{Gitlab::Git::BLANK_SHA}" }
- subject(:cleaner) { described_class.new(repository.raw) }
+ let(:clean_refs) { %W[refs/environments/1 refs/merge-requests/1 refs/keep-around/#{head_sha}] }
+ let(:keep_refs) { %w[refs/heads/_keep refs/tags/_keep] }
- describe '#apply_bfg_object_map' do
- let(:clean_refs) { %W[refs/environments/1 refs/merge-requests/1 refs/keep-around/#{head_sha}] }
- let(:keep_refs) { %w[refs/heads/_keep refs/tags/_keep] }
+ subject(:cleaner) { described_class.new(repository.raw) }
+ shared_examples_for '#apply_bfg_object_map_stream' do
before do
(clean_refs + keep_refs).each { |ref| repository.create_ref(head_sha, ref) }
end
- context 'from StringIO' do
- let(:object_map) { StringIO.new(object_map_data) }
+ it 'removes internal references' do
+ entries = []
- it 'removes internal references' do
- cleaner.apply_bfg_object_map(object_map)
+ cleaner.apply_bfg_object_map_stream(object_map) do |rsp|
+ entries.concat(rsp.entries)
+ end
- aggregate_failures do
- clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_falsy }
- keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_truthy }
- end
+ aggregate_failures do
+ clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be(false) }
+ keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be(true) }
+
+ expect(entries).to contain_exactly(
+ Gitaly::ApplyBfgObjectMapStreamResponse::Entry.new(
+ type: :COMMIT,
+ old_oid: head_sha,
+ new_oid: Gitlab::Git::BLANK_SHA
+ )
+ )
end
end
+ end
- context 'from Gitlab::HttpIO' do
- let(:url) { 'http://example.com/bfg_object_map.txt' }
- let(:tempfile) { Tempfile.new }
- let(:object_map) { Gitlab::HttpIO.new(url, object_map_data.size) }
+ describe '#apply_bfg_object_map_stream (from StringIO)' do
+ let(:object_map) { StringIO.new(object_map_data) }
- around do |example|
- begin
- tempfile.write(object_map_data)
- tempfile.close
+ include_examples '#apply_bfg_object_map_stream'
+ end
- example.run
- ensure
- tempfile.unlink
- end
- end
+ describe '#apply_bfg_object_map_stream (from Gitlab::HttpIO)' do
+ let(:url) { 'http://example.com/bfg_object_map.txt' }
+ let(:tempfile) { Tempfile.new }
+ let(:object_map) { Gitlab::HttpIO.new(url, object_map_data.size) }
- it 'removes internal references' do
- stub_remote_url_200(url, tempfile.path)
+ around do |example|
+ tempfile.write(object_map_data)
+ tempfile.close
- cleaner.apply_bfg_object_map(object_map)
+ stub_remote_url_200(url, tempfile.path)
- aggregate_failures do
- clean_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_falsy }
- keep_refs.each { |ref| expect(repository.ref_exists?(ref)).to be_truthy }
- end
- end
+ example.run
+ ensure
+ tempfile.unlink
end
+
+ include_examples '#apply_bfg_object_map_stream'
end
end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 8a9e78ba3c3..e72fb9c6fbc 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -3,6 +3,7 @@ require "spec_helper"
describe Gitlab::Git::Repository, :seed_helper do
include Gitlab::EncodingHelper
+ include RepoHelpers
using RSpec::Parameterized::TableSyntax
shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method|
@@ -28,51 +29,6 @@ describe Gitlab::Git::Repository, :seed_helper do
let(:storage_path) { TestEnv.repos_path }
let(:user) { build(:user) }
- describe '.create_hooks' do
- let(:repo_path) { File.join(storage_path, 'hook-test.git') }
- let(:hooks_dir) { File.join(repo_path, 'hooks') }
- let(:target_hooks_dir) { Gitlab.config.gitlab_shell.hooks_path }
- let(:existing_target) { File.join(repo_path, 'foobar') }
-
- before do
- FileUtils.rm_rf(repo_path)
- FileUtils.mkdir_p(repo_path)
- end
-
- context 'hooks is a directory' do
- let(:existing_file) { File.join(hooks_dir, 'my-file') }
-
- before do
- FileUtils.mkdir_p(hooks_dir)
- FileUtils.touch(existing_file)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- it { expect(Dir[File.join(repo_path, "hooks.old.*/my-file")].count).to eq(1) }
- end
-
- context 'hooks is a valid symlink' do
- before do
- FileUtils.mkdir_p existing_target
- File.symlink(existing_target, hooks_dir)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- end
-
- context 'hooks is a broken symlink' do
- before do
- FileUtils.rm_f(existing_target)
- File.symlink(existing_target, hooks_dir)
- described_class.create_hooks(repo_path, target_hooks_dir)
- end
-
- it { expect(File.readlink(hooks_dir)).to eq(target_hooks_dir) }
- end
- end
-
describe "Respond to" do
subject { repository }
@@ -95,6 +51,12 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#create_repository' do
+ it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::RepositoryService, :create_repository do
+ subject { repository.create_repository }
+ end
+ end
+
describe '#branch_names' do
subject { repository.branch_names }
@@ -152,13 +114,14 @@ describe Gitlab::Git::Repository, :seed_helper do
let(:append_sha) { true }
let(:ref) { 'master' }
let(:format) { nil }
+ let(:path) { nil }
let(:expected_extension) { 'tar.gz' }
let(:expected_filename) { "#{expected_prefix}.#{expected_extension}" }
let(:expected_path) { File.join(storage_path, cache_key, expected_filename) }
let(:expected_prefix) { "gitlab-git-test-#{ref}-#{SeedRepo::LastCommit::ID}" }
- subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha) }
+ subject(:metadata) { repository.archive_metadata(ref, storage_path, 'gitlab-git-test', format, append_sha: append_sha, path: path) }
it 'sets CommitId to the commit SHA' do
expect(metadata['CommitId']).to eq(SeedRepo::LastCommit::ID)
@@ -176,6 +139,14 @@ describe Gitlab::Git::Repository, :seed_helper do
expect(metadata['ArchivePath']).to eq(expected_path)
end
+ context 'path is set' do
+ let(:path) { 'foo/bar' }
+
+ it 'appends the path to the prefix' do
+ expect(metadata['ArchivePrefix']).to eq("#{expected_prefix}-foo-bar")
+ end
+ end
+
context 'append_sha varies archive path and filename' do
where(:append_sha, :ref, :expected_prefix) do
sha = SeedRepo::LastCommit::ID
@@ -215,6 +186,18 @@ describe Gitlab::Git::Repository, :seed_helper do
it { is_expected.to be < 2 }
end
+ describe '#object_directory_size' do
+ before do
+ allow(repository.gitaly_repository_client)
+ .to receive(:get_object_directory_size)
+ .and_return(2)
+ end
+
+ subject { repository.object_directory_size }
+
+ it { is_expected.to eq 2048 }
+ end
+
describe '#empty?' do
it { expect(repository).not_to be_empty }
end
@@ -441,20 +424,20 @@ describe Gitlab::Git::Repository, :seed_helper do
ensure_seeds
end
- it "should create a new branch" do
+ it "creates a new branch" do
expect(repository.create_branch('new_branch', 'master')).not_to be_nil
end
- it "should create a new branch with the right name" do
+ it "creates a new branch with the right name" do
expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch')
end
- it "should fail if we create an existing branch" do
+ it "fails if we create an existing branch" do
repository.create_branch('duplicated_branch', 'master')
expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists")
end
- it "should fail if we create a branch from a non existing ref" do
+ it "fails if we create a branch from a non existing ref" do
expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge")
end
end
@@ -513,7 +496,7 @@ describe Gitlab::Git::Repository, :seed_helper do
describe "#refs_hash" do
subject { repository.refs_hash }
- it "should have as many entries as branches and tags" do
+ it "has as many entries as branches and tags" do
expected_refs = SeedRepo::Repo::BRANCHES + SeedRepo::Repo::TAGS
# We flatten in case a commit is pointed at by more than one branch and/or tag
expect(subject.values.flatten.size).to eq(expected_refs.size)
@@ -522,6 +505,13 @@ describe Gitlab::Git::Repository, :seed_helper do
it 'has valid commit ids as keys' do
expect(subject.keys).to all( match(Commit::COMMIT_SHA_PATTERN) )
end
+
+ it 'does not error when dereferenced_target is nil' do
+ blob_id = repository.blob_at('master', 'README.md').id
+ repository_rugged.tags.create("refs/tags/blob-tag", blob_id)
+
+ expect { subject }.not_to raise_error
+ end
end
describe '#fetch_repository_as_mirror' do
@@ -604,11 +594,11 @@ describe Gitlab::Git::Repository, :seed_helper do
end
shared_examples 'search files by content' do
- it 'should have 2 items' do
+ it 'has 2 items' do
expect(search_results.size).to eq(2)
end
- it 'should have the correct matching line' do
+ it 'has the correct matching line' do
expect(search_results).to contain_exactly("search-files-by-content-branch:encoding/CHANGELOG\u00001\u0000search-files-by-content change\n",
"search-files-by-content-branch:anotherfile\u00001\u0000search-files-by-content change\n")
end
@@ -619,16 +609,6 @@ describe Gitlab::Git::Repository, :seed_helper do
repository.search_files_by_content('search-files-by-content', 'search-files-by-content-branch')
end
end
-
- it_should_behave_like 'search files by content' do
- let(:search_results) do
- repository.gitaly_repository_client.search_files_by_content(
- 'search-files-by-content-branch',
- 'search-files-by-content',
- chunked_response: false
- )
- end
- end
end
describe '#find_remote_root_ref' do
@@ -851,7 +831,7 @@ describe Gitlab::Git::Repository, :seed_helper do
context "where provides 'after' timestamp" do
options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
- it "should returns commits on or after that timestamp" do
+ it "returns commits on or after that timestamp" do
commits = repository.log(options)
expect(commits.size).to be > 0
@@ -864,7 +844,7 @@ describe Gitlab::Git::Repository, :seed_helper do
context "where provides 'before' timestamp" do
options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') }
- it "should returns commits on or before that timestamp" do
+ it "returns commits on or before that timestamp" do
commits = repository.log(options)
expect(commits.size).to be > 0
@@ -1065,14 +1045,14 @@ describe Gitlab::Git::Repository, :seed_helper do
end
describe '#find_branch' do
- it 'should return a Branch for master' do
+ it 'returns a Branch for master' do
branch = repository.find_branch('master')
expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
expect(branch.name).to eq('master')
end
- it 'should handle non-existent branch' do
+ it 'handles non-existent branch' do
branch = repository.find_branch('this-is-garbage')
expect(branch).to eq(nil)
@@ -1698,12 +1678,48 @@ describe Gitlab::Git::Repository, :seed_helper do
expect(repository.delete_config(*%w[does.not.exist test.foo1 test.foo2])).to be_nil
+ # Workaround for https://github.com/libgit2/rugged/issues/785: If
+ # Gitaly changes .gitconfig while Rugged has the file loaded
+ # Rugged::Repository#each_key will report stale values unless a
+ # lookup is done first.
+ expect(repository_rugged.config['test.foo1']).to be_nil
config_keys = repository_rugged.config.each_key.to_a
expect(config_keys).not_to include('test.foo1')
expect(config_keys).not_to include('test.foo2')
end
end
+ describe '#merge_to_ref' do
+ let(:repository) { mutable_repository }
+ let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
+ let(:left_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
+ let(:right_branch) { 'test-master' }
+ let(:target_ref) { 'refs/merge-requests/999/merge' }
+
+ before do
+ repository.create_branch(right_branch, branch_head) unless repository.branch_exists?(right_branch)
+ end
+
+ def merge_to_ref
+ repository.merge_to_ref(user, left_sha, right_branch, target_ref, 'Merge message')
+ end
+
+ it 'generates a commit in the target_ref' do
+ expect(repository.ref_exists?(target_ref)).to be(false)
+
+ commit_sha = merge_to_ref
+ ref_head = repository.commit(target_ref)
+
+ expect(commit_sha).to be_present
+ expect(repository.ref_exists?(target_ref)).to be(true)
+ expect(ref_head.id).to eq(commit_sha)
+ end
+
+ it 'does not change the right branch HEAD' do
+ expect { merge_to_ref }.not_to change { repository.find_branch(right_branch).target }
+ end
+ end
+
describe '#merge' do
let(:repository) { mutable_repository }
let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
@@ -1910,13 +1926,6 @@ describe Gitlab::Git::Repository, :seed_helper do
expect { imported_repo.fsck }.not_to raise_exception
end
- it 'creates a symlink to the global hooks dir' do
- imported_repo.create_from_bundle(valid_bundle_path)
- hooks_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { File.join(imported_repo.path, 'hooks') }
-
- expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
- end
-
it 'raises an error if the bundle is an attempted malicious payload' do
expect do
imported_repo.create_from_bundle(malicious_bundle_path)
@@ -1924,6 +1933,70 @@ describe Gitlab::Git::Repository, :seed_helper do
end
end
+ describe '#compare_source_branch' do
+ let(:repository) { Gitlab::Git::Repository.new('default', TEST_GITATTRIBUTES_REPO_PATH, '', 'group/project') }
+
+ context 'within same repository' do
+ it 'does not create a temp ref' do
+ expect(repository).not_to receive(:fetch_source_branch!)
+ expect(repository).not_to receive(:delete_refs)
+
+ compare = repository.compare_source_branch('master', repository, 'feature', straight: false)
+ expect(compare).to be_a(Gitlab::Git::Compare)
+ expect(compare.commits.count).to be > 0
+ end
+
+ it 'returns empty commits when source ref does not exist' do
+ compare = repository.compare_source_branch('master', repository, 'non-existent-branch', straight: false)
+
+ expect(compare.commits).to be_empty
+ end
+ end
+
+ context 'with different repositories' do
+ context 'when ref is known by source repo, but not by target' do
+ before do
+ mutable_repository.write_ref('another-branch', 'feature')
+ end
+
+ it 'creates temp ref' do
+ expect(repository).not_to receive(:fetch_source_branch!)
+ expect(repository).not_to receive(:delete_refs)
+
+ compare = repository.compare_source_branch('master', mutable_repository, 'another-branch', straight: false)
+ expect(compare).to be_a(Gitlab::Git::Compare)
+ expect(compare.commits.count).to be > 0
+ end
+ end
+
+ context 'when ref is known by source and target repos' do
+ before do
+ mutable_repository.write_ref('another-branch', 'feature')
+ repository.write_ref('another-branch', 'feature')
+ end
+
+ it 'does not create a temp ref' do
+ expect(repository).not_to receive(:fetch_source_branch!)
+ expect(repository).not_to receive(:delete_refs)
+
+ compare = repository.compare_source_branch('master', mutable_repository, 'another-branch', straight: false)
+ expect(compare).to be_a(Gitlab::Git::Compare)
+ expect(compare.commits.count).to be > 0
+ end
+ end
+
+ context 'when ref is unknown by source repo' do
+ it 'returns nil when source ref does not exist' do
+ expect(repository).to receive(:fetch_source_branch!).and_call_original
+ expect(repository).to receive(:delete_refs).and_call_original
+
+ compare = repository.compare_source_branch('master', mutable_repository, 'non-existent-branch', straight: false)
+ expect(compare).to be_nil
+ end
+ end
+ end
+ end
+
describe '#checksum' do
it 'calculates the checksum for non-empty repo' do
expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059'
@@ -2097,86 +2170,48 @@ describe Gitlab::Git::Repository, :seed_helper do
repository_rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", source_branch.dereferenced_target.sha)
end
- # Build the options hash that's passed to Rugged::Commit#create
- def commit_options(repo, index, target, ref, message)
- options = {}
- options[:tree] = index.write_tree(repo)
- options[:author] = {
- email: "test@example.com",
- name: "Test Author",
- time: Time.gm(2014, "mar", 3, 20, 15, 1)
- }
- options[:committer] = {
- email: "test@example.com",
- name: "Test Author",
- time: Time.gm(2014, "mar", 3, 20, 15, 1)
- }
- options[:message] ||= message
- options[:parents] = repo.empty? ? [] : [target].compact
- options[:update_ref] = ref
-
- options
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of CHANGELOG with a single new line of text.
- def new_commit_edit_old_file(repo)
- oid = repo.write("I replaced the changelog with this text", :blob)
- index = repo.index
- index.read_tree(repo.head.target.tree)
- index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
-
- options = commit_options(
- repo,
- index,
- repo.head.target,
- "HEAD",
- "Edit CHANGELOG in its original location"
- )
-
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of the specified file_path with new text.
- def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head)
- oid = repo.write(text, :blob)
- index = repo.index
- index.read_tree(branch.target.tree)
- index.add(path: file_path, oid: oid, mode: 0100644)
- options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message)
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
- # contents of encoding/CHANGELOG with new text.
- def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text)
- branch = repo.branches[branch_name]
- new_commit_edit_new_file(repo, file_path, commit_message, text, branch)
- end
-
- # Writes a new commit to the repo and returns a Rugged::Commit. Moves the
- # CHANGELOG file to the encoding/ directory.
- def new_commit_move_file(repo)
- blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
- file_content = repo.lookup(blob_oid).content
- oid = repo.write(file_content, :blob)
- index = repo.index
- index.read_tree(repo.head.target.tree)
- index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
- index.remove("CHANGELOG")
-
- options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/")
-
- sha = Rugged::Commit.create(repo, options)
- repo.lookup(sha)
- end
-
def refs(dir)
IO.popen(%W[git -C #{dir} for-each-ref], &:read).split("\n").map do |line|
line.split("\t").last
end
end
+
+ describe '#disconnect_alternates' do
+ let(:project) { create(:project, :repository) }
+ let(:pool_repository) { create(:pool_repository) }
+ let(:repository) { project.repository }
+ let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) }
+ let(:object_pool) { pool_repository.object_pool }
+ let(:object_pool_path) { File.join(TestEnv.repos_path, object_pool.repository.relative_path) }
+ let(:object_pool_rugged) { Rugged::Repository.new(object_pool_path) }
+
+ before do
+ object_pool.create
+ end
+
+ it 'does not raise an error when disconnecting a non-linked repository' do
+ expect { repository.disconnect_alternates }.not_to raise_error
+ end
+
+ it 'removes the alternates file' do
+ object_pool.link(repository)
+
+ alternates_file = File.join(repository_path, "objects", "info", "alternates")
+ expect(File.exist?(alternates_file)).to be_truthy
+
+ repository.disconnect_alternates
+
+ expect(File.exist?(alternates_file)).to be_falsey
+ end
+
+ it 'can still access objects in the object pool' do
+ object_pool.link(repository)
+ new_commit = new_commit_edit_old_file(object_pool_rugged)
+ expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid)
+
+ repository.disconnect_alternates
+
+ expect(repository.commit(new_commit.oid).id).to eq(new_commit.oid)
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 4a4d69490a3..7e169cfe270 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -3,7 +3,7 @@ require "spec_helper"
describe Gitlab::Git::Tree, :seed_helper do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') }
- context :repo do
+ shared_examples :repo do
let(:tree) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID) }
it { expect(tree).to be_kind_of Array }
@@ -12,6 +12,19 @@ describe Gitlab::Git::Tree, :seed_helper do
it { expect(tree.select(&:file?).size).to eq(10) }
it { expect(tree.select(&:submodule?).size).to eq(2) }
+ it 'returns an empty array when called with an invalid ref' do
+ expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([])
+ end
+
+ it 'returns a list of tree objects' do
+ entries = described_class.where(repository, SeedRepo::Commit::ID, 'files', true)
+
+ expect(entries.map(&:path)).to include('files/html',
+ 'files/markdown/ruby-style-guide.md')
+ expect(entries.count).to be >= 10
+ expect(entries).to all(be_a(Gitlab::Git::Tree))
+ end
+
describe '#dir?' do
let(:dir) { tree.select(&:dir?).first }
@@ -20,8 +33,8 @@ describe Gitlab::Git::Tree, :seed_helper do
it { expect(dir.commit_id).to eq(SeedRepo::Commit::ID) }
it { expect(dir.name).to eq('encoding') }
it { expect(dir.path).to eq('encoding') }
- it { expect(dir.flat_path).to eq('encoding') }
it { expect(dir.mode).to eq('40000') }
+ it { expect(dir.flat_path).to eq('encoding') }
context :subdir do
let(:subdir) { Gitlab::Git::Tree.where(repository, SeedRepo::Commit::ID, 'files').first }
@@ -44,6 +57,51 @@ describe Gitlab::Git::Tree, :seed_helper do
it { expect(subdir_file.path).to eq('files/ruby/popen.rb') }
it { expect(subdir_file.flat_path).to eq('files/ruby/popen.rb') }
end
+
+ context :flat_path do
+ let(:filename) { 'files/flat/path/correct/content.txt' }
+ let(:oid) { create_file(filename) }
+ let(:subdir_file) { Gitlab::Git::Tree.where(repository, oid, 'files/flat').first }
+ let(:repository_rugged) { Rugged::Repository.new(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH)) }
+
+ it { expect(subdir_file.flat_path).to eq('files/flat/path/correct') }
+ end
+
+ def create_file(path)
+ oid = repository_rugged.write('test', :blob)
+ index = repository_rugged.index
+ index.add(path: filename, oid: oid, mode: 0100644)
+
+ options = commit_options(
+ repository_rugged,
+ index,
+ repository_rugged.head.target,
+ 'HEAD',
+ 'Add new file')
+
+ Rugged::Commit.create(repository_rugged, options)
+ end
+
+ # Build the options hash that's passed to Rugged::Commit#create
+ def commit_options(repo, index, target, ref, message)
+ options = {}
+ options[:tree] = index.write_tree(repo)
+ options[:author] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:committer] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:message] ||= message
+ options[:parents] = repo.empty? ? [] : [target].compact
+ options[:update_ref] = ref
+
+ options
+ end
end
describe '#file?' do
@@ -79,9 +137,17 @@ describe Gitlab::Git::Tree, :seed_helper do
end
end
- describe '#where' do
- it 'returns an empty array when called with an invalid ref' do
- expect(described_class.where(repository, 'foobar-does-not-exist')).to eq([])
+ describe '.where with Gitaly enabled' do
+ it_behaves_like :repo
+ end
+
+ describe '.where with Rugged enabled', :enable_rugged do
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged).to receive(:lookup).with(SeedRepo::Commit::ID)
+
+ described_class.where(repository, SeedRepo::Commit::ID, 'files', false)
end
+
+ it_behaves_like :repo
end
end
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
index ded5d7576df..1e577392949 100644
--- a/spec/lib/gitlab/git/wiki_spec.rb
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -21,13 +21,13 @@ describe Gitlab::Git::Wiki do
end
it 'returns all the pages' do
- expect(subject.pages.count).to eq(2)
- expect(subject.pages.first.title).to eq 'page1'
- expect(subject.pages.last.title).to eq 'page2'
+ expect(subject.list_pages.count).to eq(2)
+ expect(subject.list_pages.first.title).to eq 'page1'
+ expect(subject.list_pages.last.title).to eq 'page2'
end
it 'returns only one page' do
- pages = subject.pages(limit: 1)
+ pages = subject.list_pages(limit: 1)
expect(pages.count).to eq(1)
expect(pages.first.title).to eq 'page1'
@@ -62,8 +62,8 @@ describe Gitlab::Git::Wiki do
subject.delete_page('*', commit_details('whatever'))
- expect(subject.pages.count).to eq 1
- expect(subject.pages.first.title).to eq 'page1'
+ expect(subject.list_pages.count).to eq 1
+ expect(subject.list_pages.first.title).to eq 'page1'
end
end
@@ -87,7 +87,7 @@ describe Gitlab::Git::Wiki do
with_them do
subject { wiki.preview_slug(title, format) }
- let(:gitaly_slug) { wiki.pages.first }
+ let(:gitaly_slug) { wiki.list_pages.first }
it { is_expected.to eq(expected_slug) }
@@ -96,7 +96,7 @@ describe Gitlab::Git::Wiki do
create_page(title, 'content', format: format)
- gitaly_slug = wiki.pages.first.url_path
+ gitaly_slug = wiki.list_pages.first.url_path
is_expected.to eq(gitaly_slug)
end
diff --git a/spec/lib/gitlab/git_ref_validator_spec.rb b/spec/lib/gitlab/git_ref_validator_spec.rb
index 3ab04a1c46d..b63389af29f 100644
--- a/spec/lib/gitlab/git_ref_validator_spec.rb
+++ b/spec/lib/gitlab/git_ref_validator_spec.rb
@@ -1,31 +1,69 @@
require 'spec_helper'
describe Gitlab::GitRefValidator do
- it { expect(described_class.validate('feature/new')).to be_truthy }
- it { expect(described_class.validate('implement_@all')).to be_truthy }
- it { expect(described_class.validate('my_new_feature')).to be_truthy }
- it { expect(described_class.validate('my-branch')).to be_truthy }
- it { expect(described_class.validate('#1')).to be_truthy }
- it { expect(described_class.validate('feature/refs/heads/foo')).to be_truthy }
- it { expect(described_class.validate('feature/~new/')).to be_falsey }
- it { expect(described_class.validate('feature/^new/')).to be_falsey }
- it { expect(described_class.validate('feature/:new/')).to be_falsey }
- it { expect(described_class.validate('feature/?new/')).to be_falsey }
- it { expect(described_class.validate('feature/*new/')).to be_falsey }
- it { expect(described_class.validate('feature/[new/')).to be_falsey }
- it { expect(described_class.validate('feature/new/')).to be_falsey }
- it { expect(described_class.validate('feature/new.')).to be_falsey }
- it { expect(described_class.validate('feature\@{')).to be_falsey }
- it { expect(described_class.validate('feature\new')).to be_falsey }
- it { expect(described_class.validate('feature//new')).to be_falsey }
- it { expect(described_class.validate('feature new')).to be_falsey }
- it { expect(described_class.validate('refs/heads/')).to be_falsey }
- it { expect(described_class.validate('refs/remotes/')).to be_falsey }
- it { expect(described_class.validate('refs/heads/feature')).to be_falsey }
- it { expect(described_class.validate('refs/remotes/origin')).to be_falsey }
- it { expect(described_class.validate('-')).to be_falsey }
- it { expect(described_class.validate('-branch')).to be_falsey }
- it { expect(described_class.validate('.tag')).to be_falsey }
- it { expect(described_class.validate('my branch')).to be_falsey }
- it { expect(described_class.validate("\xA0\u0000\xB0")).to be_falsey }
+ using RSpec::Parameterized::TableSyntax
+
+ context '.validate' do
+ it { expect(described_class.validate('feature/new')).to be true }
+ it { expect(described_class.validate('implement_@all')).to be true }
+ it { expect(described_class.validate('my_new_feature')).to be true }
+ it { expect(described_class.validate('my-branch')).to be true }
+ it { expect(described_class.validate('#1')).to be true }
+ it { expect(described_class.validate('feature/refs/heads/foo')).to be true }
+ it { expect(described_class.validate('feature/~new/')).to be false }
+ it { expect(described_class.validate('feature/^new/')).to be false }
+ it { expect(described_class.validate('feature/:new/')).to be false }
+ it { expect(described_class.validate('feature/?new/')).to be false }
+ it { expect(described_class.validate('feature/*new/')).to be false }
+ it { expect(described_class.validate('feature/[new/')).to be false }
+ it { expect(described_class.validate('feature/new/')).to be false }
+ it { expect(described_class.validate('feature/new.')).to be false }
+ it { expect(described_class.validate('feature\@{')).to be false }
+ it { expect(described_class.validate('feature\new')).to be false }
+ it { expect(described_class.validate('feature//new')).to be false }
+ it { expect(described_class.validate('feature new')).to be false }
+ it { expect(described_class.validate('refs/heads/')).to be false }
+ it { expect(described_class.validate('refs/remotes/')).to be false }
+ it { expect(described_class.validate('refs/heads/feature')).to be false }
+ it { expect(described_class.validate('refs/remotes/origin')).to be false }
+ it { expect(described_class.validate('-')).to be false }
+ it { expect(described_class.validate('-branch')).to be false }
+ it { expect(described_class.validate('+foo:bar')).to be false }
+ it { expect(described_class.validate('foo:bar')).to be false }
+ it { expect(described_class.validate('.tag')).to be false }
+ it { expect(described_class.validate('my branch')).to be false }
+ it { expect(described_class.validate("\xA0\u0000\xB0")).to be false }
+ end
+
+ context '.validate_merge_request_branch' do
+ it { expect(described_class.validate_merge_request_branch('HEAD')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/new')).to be true }
+ it { expect(described_class.validate_merge_request_branch('implement_@all')).to be true }
+ it { expect(described_class.validate_merge_request_branch('my_new_feature')).to be true }
+ it { expect(described_class.validate_merge_request_branch('my-branch')).to be true }
+ it { expect(described_class.validate_merge_request_branch('#1')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/refs/heads/foo')).to be true }
+ it { expect(described_class.validate_merge_request_branch('feature/~new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/^new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/:new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/?new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/*new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/[new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/new/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature/new.')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature\@{')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature\new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature//new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('feature new')).to be false }
+ it { expect(described_class.validate_merge_request_branch('refs/heads/master')).to be true }
+ it { expect(described_class.validate_merge_request_branch('refs/heads/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('refs/remotes/')).to be false }
+ it { expect(described_class.validate_merge_request_branch('-')).to be false }
+ it { expect(described_class.validate_merge_request_branch('-branch')).to be false }
+ it { expect(described_class.validate_merge_request_branch('+foo:bar')).to be false }
+ it { expect(described_class.validate_merge_request_branch('foo:bar')).to be false }
+ it { expect(described_class.validate_merge_request_branch('.tag')).to be false }
+ it { expect(described_class.validate_merge_request_branch('my branch')).to be false }
+ it { expect(described_class.validate_merge_request_branch("\xA0\u0000\xB0")).to be false }
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
index 369deff732a..c42332dc27b 100644
--- a/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/cleanup_service_spec.rb
@@ -6,14 +6,14 @@ describe Gitlab::GitalyClient::CleanupService do
let(:relative_path) { project.disk_path + '.git' }
let(:client) { described_class.new(project.repository) }
- describe '#apply_bfg_object_map' do
- it 'sends an apply_bfg_object_map message' do
+ describe '#apply_bfg_object_map_stream' do
+ it 'sends an apply_bfg_object_map_stream message' do
expect_any_instance_of(Gitaly::CleanupService::Stub)
- .to receive(:apply_bfg_object_map)
+ .to receive(:apply_bfg_object_map_stream)
.with(kind_of(Enumerator), kind_of(Hash))
- .and_return(double)
+ .and_return([])
- client.apply_bfg_object_map(StringIO.new)
+ client.apply_bfg_object_map_stream(StringIO.new)
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index d7bd757149d..6d6107ca3e7 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -221,6 +221,21 @@ describe Gitlab::GitalyClient::CommitService do
expect(commit).to eq(commit_dbl)
end
end
+
+ context 'when caching of the ref name is enabled' do
+ it 'returns a cached commit' do
+ expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commit).once.and_return(double(commit: commit_dbl))
+
+ commit = nil
+ 2.times do
+ ::Gitlab::GitalyClient.allow_ref_name_caching do
+ commit = described_class.new(repository).find_commit('master')
+ end
+ end
+
+ expect(commit).to eq(commit_dbl)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
index 149b7ec5bb0..0e0c3d329b5 100644
--- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb
@@ -43,4 +43,24 @@ describe Gitlab::GitalyClient::ObjectPoolService do
end
end
end
+
+ describe '#fetch' do
+ before do
+ subject.delete
+ end
+
+ it 'restores the pool repository objects' do
+ subject.fetch(project.repository)
+
+ expect(object_pool.repository.exists?).to be(true)
+ end
+
+ context 'when called twice' do
+ it "doesn't raise an error" do
+ subject.delete
+
+ expect { subject.fetch(project.repository) }.not_to raise_error
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
index b37fe2686b6..7579a6577b9 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -39,7 +39,7 @@ describe Gitlab::GitalyClient::OperationService do
context "when pre_receive_error is present" do
let(:response) do
- Gitaly::UserCreateBranchResponse.new(pre_receive_error: "something failed")
+ Gitaly::UserCreateBranchResponse.new(pre_receive_error: "GitLab: something failed")
end
it "throws a PreReceive exception" do
@@ -80,7 +80,7 @@ describe Gitlab::GitalyClient::OperationService do
context "when pre_receive_error is present" do
let(:response) do
- Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "something failed")
+ Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "GitLab: something failed")
end
it "throws a PreReceive exception" do
@@ -117,7 +117,7 @@ describe Gitlab::GitalyClient::OperationService do
context "when pre_receive_error is present" do
let(:response) do
- Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "something failed")
+ Gitaly::UserDeleteBranchResponse.new(pre_receive_error: "GitLab: something failed")
end
it "throws a PreReceive exception" do
@@ -175,7 +175,7 @@ describe Gitlab::GitalyClient::OperationService do
shared_examples 'cherry pick and revert errors' do
context 'when a pre_receive_error is present' do
- let(:response) { response_class.new(pre_receive_error: "something failed") }
+ let(:response) { response_class.new(pre_receive_error: "GitLab: something failed") }
it 'raises a PreReceiveError' do
expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed")
@@ -313,7 +313,7 @@ describe Gitlab::GitalyClient::OperationService do
end
context 'when a pre_receive_error is present' do
- let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "something failed") }
+ let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "GitLab: something failed") }
it 'raises a PreReceiveError' do
expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed")
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 400d426c949..0bb6e582159 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -89,6 +89,16 @@ describe Gitlab::GitalyClient::RefService do
end
end
+ describe '#list_new_blobs' do
+ it 'raises DeadlineExceeded when timeout is too small' do
+ newrev = '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51'
+
+ expect do
+ client.list_new_blobs(newrev, dynamic_timeout: 0.001)
+ end.to raise_error(GRPC::DeadlineExceeded)
+ end
+ end
+
describe '#local_branches' do
it 'sends a find_local_branches message' do
expect_any_instance_of(Gitaly::RefService::Stub)
@@ -155,4 +165,15 @@ describe Gitlab::GitalyClient::RefService do
client.delete_refs(except_with_prefixes: prefixes)
end
end
+
+ describe '#pack_refs' do
+ it 'sends a pack_refs message' do
+ expect_any_instance_of(Gitaly::RefService::Stub)
+ .to receive(:pack_refs)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(double(:pack_refs_response))
+
+ client.pack_refs
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
index 46ca2340389..a3808adb376 100644
--- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -73,6 +73,17 @@ describe Gitlab::GitalyClient::RepositoryService do
end
end
+ describe '#get_object_directory_size' do
+ it 'sends a get_object_directory_size message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:get_object_directory_size)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_return(size: 0)
+
+ client.get_object_directory_size
+ end
+ end
+
describe '#apply_gitattributes' do
let(:revision) { 'master' }
@@ -231,4 +242,34 @@ describe Gitlab::GitalyClient::RepositoryService do
client.raw_changes_between('deadbeef', 'deadpork')
end
end
+
+ describe '#disconnect_alternates' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:repository_path) { File.join(TestEnv.repos_path, repository.relative_path) }
+ let(:pool_repository) { create(:pool_repository) }
+ let(:object_pool) { pool_repository.object_pool }
+ let(:object_pool_service) { Gitlab::GitalyClient::ObjectPoolService.new(object_pool) }
+
+ before do
+ object_pool_service.create(repository)
+ object_pool_service.link_repository(repository)
+ end
+
+ it 'deletes the alternates file' do
+ repository.disconnect_alternates
+
+ alternates_file = File.join(repository_path, "objects", "info", "alternates")
+
+ expect(File.exist?(alternates_file)).to be_falsey
+ end
+
+ context 'when called twice' do
+ it "doesn't raise an error" do
+ repository.disconnect_alternates
+
+ expect { repository.disconnect_alternates }.not_to raise_error
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
index c89913ec8e9..bb10be2a4dc 100644
--- a/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/storage_settings_spec.rb
@@ -26,4 +26,14 @@ describe Gitlab::GitalyClient::StorageSettings do
end
end
end
+
+ describe '.disk_access_denied?' do
+ it 'return false when Rugged is enabled', :enable_rugged do
+ expect(described_class.disk_access_denied?).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(described_class.disk_access_denied?).to be_truthy
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
index d82c9c28da0..4fa8e97aca0 100644
--- a/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/wiki_service_spec.rb
@@ -46,7 +46,7 @@ describe Gitlab::GitalyClient::WikiService do
end
end
- describe '#get_all_pages' do
+ describe '#load_all_pages' do
let(:page_2_info) { { title: 'My Page 2', raw_data: 'c', version: page_version } }
let(:response) do
[
@@ -63,7 +63,7 @@ describe Gitlab::GitalyClient::WikiService do
let(:wiki_page_2) { subject[1].first }
let(:wiki_page_2_version) { subject[1].last }
- subject { client.get_all_pages }
+ subject { client.load_all_pages }
it 'sends a wiki_get_all_pages message' do
expect_any_instance_of(Gitaly::WikiService::Stub)
@@ -99,7 +99,7 @@ describe Gitlab::GitalyClient::WikiService do
end
context 'with limits' do
- subject { client.get_all_pages(limit: 1) }
+ subject { client.load_all_pages(limit: 1) }
it 'sends a request with the limit' do
expect_any_instance_of(Gitaly::WikiService::Stub)
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index cf12baf1a93..da1eb0c2618 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -142,6 +142,48 @@ describe Gitlab::GitalyClient do
end
end
+ describe '.request_kwargs' do
+ context 'when catfile-cache feature is enabled' do
+ before do
+ stub_feature_flags('gitaly_catfile-cache': true)
+ end
+
+ it 'sets the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', nil)
+ expect(results[:metadata]).to include('gitaly-session-id')
+ end
+
+ context 'when RequestStore is not enabled' do
+ it 'sets a different gitaly-session-id per request' do
+ gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+
+ expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).not_to eq(gitaly_session_id)
+ end
+ end
+
+ context 'when RequestStore is enabled', :request_store do
+ it 'sets the same gitaly-session-id on every outgoing request metadata' do
+ gitaly_session_id = described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']
+
+ 3.times do
+ expect(described_class.request_kwargs('default', nil)[:metadata]['gitaly-session-id']).to eq(gitaly_session_id)
+ end
+ end
+ end
+ end
+
+ context 'when catfile-cache feature is disabled' do
+ before do
+ stub_feature_flags({ 'gitaly_catfile-cache': false })
+ end
+
+ it 'does not set the gitaly-session-id in the metadata' do
+ results = described_class.request_kwargs('default', nil)
+ expect(results[:metadata]).not_to include('gitaly-session-id')
+ end
+ end
+ end
+
describe 'enforce_gitaly_request_limits?' do
def call_gitaly(count = 1)
(1..count).each do
@@ -149,11 +191,21 @@ describe Gitlab::GitalyClient do
end
end
- context 'when RequestStore is enabled', :request_store do
+ context 'when RequestStore is enabled and the maximum number of calls is not enforced by a feature flag', :request_store do
+ before do
+ stub_feature_flags(gitaly_enforce_requests_limits: false)
+ end
+
it 'allows up the maximum number of allowed calls' do
expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS) }.not_to raise_error
end
+ it 'allows the maximum number of calls to be exceeded if GITALY_DISABLE_REQUEST_LIMITS is set' do
+ stub_env('GITALY_DISABLE_REQUEST_LIMITS', 'true')
+
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.not_to raise_error
+ end
+
context 'when the maximum number of calls has been reached' do
before do
call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS)
@@ -189,6 +241,32 @@ describe Gitlab::GitalyClient do
end
end
+ context 'in production and when RequestStore is enabled', :request_store do
+ before do
+ allow(Rails.env).to receive(:production?).and_return(true)
+ end
+
+ context 'when the maximum number of calls is enforced by a feature flag' do
+ before do
+ stub_feature_flags(gitaly_enforce_requests_limits: true)
+ end
+
+ it 'does not allow the maximum number of calls to be exceeded' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.to raise_error(Gitlab::GitalyClient::TooManyInvocationsError)
+ end
+ end
+
+ context 'when the maximum number of calls is not enforced by a feature flag' do
+ before do
+ stub_feature_flags(gitaly_enforce_requests_limits: false)
+ end
+
+ it 'allows the maximum number of calls to be exceeded' do
+ expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 1) }.not_to raise_error
+ end
+ end
+ end
+
context 'when RequestStore is not active' do
it 'does not raise errors when the maximum number of allowed calls is exceeded' do
expect { call_gitaly(Gitlab::GitalyClient::MAXIMUM_GITALY_CALLS + 2) }.not_to raise_error
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 7901ae005d9..dab5767ece1 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -98,6 +98,7 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
description: 'This is my issue',
milestone_id: milestone.id,
state: :opened,
+ state_id: 1,
created_at: created_at,
updated_at: updated_at
},
@@ -127,6 +128,7 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
description: "*Created by: alice*\n\nThis is my issue",
milestone_id: milestone.id,
state: :opened,
+ state_id: 1,
created_at: created_at,
updated_at: updated_at
},
diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
index b1cac3b6e46..120a07ff2b3 100644
--- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis
let(:project) { create(:project, import_source: 'foo/bar') }
let(:client) { double(:client) }
let(:importer) { described_class.new(project, client) }
+ let(:due_on) { Time.new(2017, 2, 1, 12, 00) }
let(:created_at) { Time.new(2017, 1, 1, 12, 00) }
let(:updated_at) { Time.new(2017, 1, 1, 12, 15) }
@@ -14,6 +15,20 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis
title: '1.0',
description: 'The first release',
state: 'open',
+ due_on: due_on,
+ created_at: created_at,
+ updated_at: updated_at
+ )
+ end
+
+ let(:milestone2) do
+ double(
+ :milestone,
+ number: 1,
+ title: '1.0',
+ description: 'The first release',
+ state: 'open',
+ due_on: nil,
created_at: created_at,
updated_at: updated_at
)
@@ -72,6 +87,7 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis
describe '#build' do
let(:milestone_hash) { importer.build(milestone) }
+ let(:milestone_hash2) { importer.build(milestone2) }
it 'returns the attributes of the milestone as a Hash' do
expect(milestone_hash).to be_an_instance_of(Hash)
@@ -98,6 +114,14 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis
expect(milestone_hash[:state]).to eq(:active)
end
+ it 'includes the due date' do
+ expect(milestone_hash[:due_date]).to eq(due_on.to_date)
+ end
+
+ it 'responds correctly to no due date value' do
+ expect(milestone_hash2[:due_date]).to be nil
+ end
+
it 'includes the created timestamp' do
expect(milestone_hash[:created_at]).to eq(created_at)
end
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index 15e59718dce..6d614c6527a 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -11,6 +11,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
let(:source_commit) { project.repository.commit('feature') }
let(:target_commit) { project.repository.commit('master') }
let(:milestone) { create(:milestone, project: project) }
+ let(:state) { :closed }
let(:pull_request) do
alice = Gitlab::GithubImport::Representation::User.new(id: 4, login: 'alice')
@@ -26,13 +27,13 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
source_repository_id: 400,
target_repository_id: 200,
source_repository_owner: 'alice',
- state: :closed,
+ state: state,
milestone_number: milestone.iid,
author: alice,
assignee: alice,
created_at: created_at,
updated_at: updated_at,
- merged_at: merged_at
+ merged_at: state == :closed && merged_at
)
end
@@ -92,6 +93,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
source_branch: 'github/fork/alice/feature',
target_branch: 'master',
state: :merged,
+ state_id: 3,
milestone_id: milestone.id,
author_id: user.id,
assignee_id: user.id,
@@ -137,6 +139,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
source_branch: 'github/fork/alice/feature',
target_branch: 'master',
state: :merged,
+ state_id: 3,
milestone_id: milestone.id,
author_id: project.creator_id,
assignee_id: user.id,
@@ -183,6 +186,7 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
source_branch: 'master-42',
target_branch: 'master',
state: :merged,
+ state_id: 3,
milestone_id: milestone.id,
author_id: user.id,
assignee_id: user.id,
@@ -260,53 +264,63 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
end
it 'does not create the source branch if merge request is merged' do
- mr, exists = importer.create_merge_request
-
- importer.insert_git_data(mr, exists)
+ mr = insert_git_data
expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
end
- it 'creates the source branch if merge request is open' do
- mr, exists = importer.create_merge_request
- mr.state = 'opened'
- mr.save
+ context 'when merge request is open' do
+ let(:state) { :opened }
- importer.insert_git_data(mr, exists)
+ it 'creates the source branch' do
+ # Ensure the project creator is creating the branches because the
+ # merge request author may not have access to push to this
+ # repository. The project owner may also be a group.
+ allow(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original
- expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy
- expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
- end
+ mr = insert_git_data
- it 'ignores Git errors when creating a branch' do
- mr, exists = importer.create_merge_request
- mr.state = 'opened'
- mr.save
+ expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy
+ expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
+ end
- expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError)
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original
+ it 'is able to retry on pre-receive errors' do
+ expect(importer).to receive(:insert_or_replace_git_data).twice.and_call_original
+ expect(project.repository).to receive(:add_branch).and_raise('exception')
- importer.insert_git_data(mr, exists)
+ expect { insert_git_data }.to raise_error('exception')
- expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
- expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
+ expect(project.repository).to receive(:add_branch).with(project.creator, anything, anything).and_call_original
+
+ mr = insert_git_data
+
+ expect(project.repository.branch_exists?(mr.source_branch)).to be_truthy
+ expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
+ expect(mr.merge_request_diffs).to be_one
+ end
+
+ it 'ignores Git command errors when creating a branch' do
+ expect(project.repository).to receive(:add_branch).and_raise(Gitlab::Git::CommandError)
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception).and_call_original
+
+ mr = insert_git_data
+
+ expect(project.repository.branch_exists?(mr.source_branch)).to be_falsey
+ expect(project.repository.branch_exists?(mr.target_branch)).to be_truthy
+ end
end
it 'creates the merge request diffs' do
- mr, exists = importer.create_merge_request
-
- importer.insert_git_data(mr, exists)
+ mr = insert_git_data
expect(mr.merge_request_diffs.exists?).to eq(true)
end
it 'creates the merge request diff commits' do
- mr, exists = importer.create_merge_request
-
- importer.insert_git_data(mr, exists)
+ mr = insert_git_data
- diff = mr.merge_request_diffs.take
+ diff = mr.merge_request_diffs.reload.first
expect(diff.merge_request_diff_commits.exists?).to eq(true)
end
@@ -322,5 +336,11 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
expect(mr.merge_request_diffs.exists?).to eq(true)
end
end
+
+ def insert_git_data
+ mr, exists = importer.create_merge_request
+ importer.insert_git_data(mr, exists)
+ mr
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 47233ea6ee2..705df1f4fe7 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -179,6 +179,17 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
describe '#import_repository' do
it 'imports the repository' do
+ repo = double(:repo, default_branch: 'develop')
+
+ expect(client)
+ .to receive(:repository)
+ .with('foo/bar')
+ .and_return(repo)
+
+ expect(project)
+ .to receive(:change_head)
+ .with('develop')
+
expect(project)
.to receive(:ensure_repository)
@@ -186,6 +197,11 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
.to receive(:fetch_as_mirror)
.with(project.import_url, refmap: Gitlab::GithubImport.refmap, forced: true, remote_name: 'github')
+ service = double
+ expect(Projects::HousekeepingService)
+ .to receive(:new).with(project, :gc).and_return(service)
+ expect(service).to receive(:execute)
+
expect(importer.import_repository).to eq(true)
end
diff --git a/spec/lib/gitlab/github_import/parallel_importer_spec.rb b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
index f5df38c9aaf..ecab64a372a 100644
--- a/spec/lib/gitlab/github_import/parallel_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/parallel_importer_spec.rb
@@ -25,18 +25,9 @@ describe Gitlab::GithubImport::ParallelImporter do
end
it 'sets the JID in Redis' do
- expect(Gitlab::SidekiqStatus)
- .to receive(:set)
- .with("github-importer/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
- .and_call_original
+ expect(Gitlab::Import::SetAsyncJid).to receive(:set_jid).with(project).and_call_original
importer.execute
end
-
- it 'updates the import JID of the project' do
- importer.execute
-
- expect(project.import_state.reload.jid).to eq("github-importer/#{project.id}")
- end
end
end
diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
new file mode 100644
index 00000000000..f06a2448ff7
--- /dev/null
+++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::GlRepository::RepoType do
+ set(:project) { create(:project) }
+
+ shared_examples 'a repo type' do
+ describe "#identifier_for_subject" do
+ subject { described_class.identifier_for_subject(project) }
+
+ it { is_expected.to eq(expected_identifier) }
+ end
+
+ describe "#fetch_id" do
+ it "finds an id match in the identifier" do
+ expect(described_class.fetch_id(expected_identifier)).to eq(expected_id)
+ end
+
+ it 'does not break on other identifiers' do
+ expect(described_class.fetch_id("wiki-noid")).to eq(nil)
+ end
+ end
+
+ describe "#path_suffix" do
+ subject { described_class.path_suffix }
+
+ it { is_expected.to eq(expected_suffix) }
+ end
+
+ describe "#repository_for" do
+ it "finds the repository for the repo type" do
+ expect(described_class.repository_for(project)).to eq(expected_repository)
+ end
+ end
+ end
+
+ describe Gitlab::GlRepository::PROJECT do
+ it_behaves_like 'a repo type' do
+ let(:expected_identifier) { "project-#{project.id}" }
+ let(:expected_id) { project.id.to_s }
+ let(:expected_suffix) { "" }
+ let(:expected_repository) { project.repository }
+ end
+
+ it "knows its type" do
+ expect(described_class).not_to be_wiki
+ expect(described_class).to be_project
+ end
+ end
+
+ describe Gitlab::GlRepository::WIKI do
+ it_behaves_like 'a repo type' do
+ let(:expected_identifier) { "wiki-#{project.id}" }
+ let(:expected_id) { project.id.to_s }
+ let(:expected_suffix) { ".wiki" }
+ let(:expected_repository) { project.wiki.repository }
+ end
+
+ it "knows its type" do
+ expect(described_class).to be_wiki
+ expect(described_class).not_to be_project
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gl_repository_spec.rb b/spec/lib/gitlab/gl_repository_spec.rb
index 4e09020471b..d4b6c629659 100644
--- a/spec/lib/gitlab/gl_repository_spec.rb
+++ b/spec/lib/gitlab/gl_repository_spec.rb
@@ -5,11 +5,11 @@ describe ::Gitlab::GlRepository do
set(:project) { create(:project, :repository) }
it 'parses a project gl_repository' do
- expect(described_class.parse("project-#{project.id}")).to eq([project, false])
+ expect(described_class.parse("project-#{project.id}")).to eq([project, Gitlab::GlRepository::PROJECT])
end
it 'parses a wiki gl_repository' do
- expect(described_class.parse("wiki-#{project.id}")).to eq([project, true])
+ expect(described_class.parse("wiki-#{project.id}")).to eq([project, Gitlab::GlRepository::WIKI])
end
it 'throws an argument error on an invalid gl_repository' do
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
new file mode 100644
index 00000000000..aec9c4baf0a
--- /dev/null
+++ b/spec/lib/gitlab/graphql/authorize/authorize_field_service_spec.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Also see spec/graphql/features/authorization_spec.rb for
+# integration tests of AuthorizeFieldService
+describe Gitlab::Graphql::Authorize::AuthorizeFieldService do
+ def type(type_authorizations = [])
+ Class.new(Types::BaseObject) do
+ graphql_name "TestType"
+
+ authorize type_authorizations
+ end
+ end
+
+ def type_with_field(field_type, field_authorizations = [], resolved_value = "Resolved value")
+ Class.new(Types::BaseObject) do
+ graphql_name "TestTypeWithField"
+ field :test_field, field_type, null: true, authorize: field_authorizations, resolve: -> (_, _, _) { resolved_value}
+ end
+ end
+
+ let(:current_user) { double(:current_user) }
+ subject(:service) { described_class.new(field) }
+
+ describe "#authorized_resolve" do
+ let(:presented_object) { double("presented object") }
+ let(:presented_type) { double("parent type", object: presented_object) }
+ subject(:resolved) { service.authorized_resolve.call(presented_type, {}, { current_user: current_user }) }
+
+ context "scalar types" do
+ shared_examples "checking permissions on the presented object" do
+ it "checks the abilities on the object being presented and returns the value" do
+ expected_permissions.each do |permission|
+ spy_ability_check_for(permission, presented_object, passed: true)
+ end
+
+ expect(resolved).to eq("Resolved value")
+ end
+
+ it "returns nil if the value wasn't authorized" do
+ allow(Ability).to receive(:allowed?).and_return false
+
+ expect(resolved).to be_nil
+ end
+ end
+
+ context "when the field is a built-in scalar type" do
+ let(:field) { type_with_field(GraphQL::STRING_TYPE, :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
+
+ it_behaves_like "checking permissions on the presented object"
+ end
+
+ context "when the field is a list of scalar types" do
+ let(:field) { type_with_field([GraphQL::STRING_TYPE], :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
+
+ it_behaves_like "checking permissions on the presented object"
+ end
+
+ context "when the field is sub-classed scalar type" do
+ let(:field) { type_with_field(Types::TimeType, :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
+
+ it_behaves_like "checking permissions on the presented object"
+ end
+
+ context "when the field is a list of sub-classed scalar types" do
+ let(:field) { type_with_field([Types::TimeType], :read_field).fields["testField"].to_graphql }
+ let(:expected_permissions) { [:read_field] }
+
+ it_behaves_like "checking permissions on the presented object"
+ end
+ end
+
+ context "when the field is a specific type" do
+ let(:custom_type) { type(:read_type) }
+ let(:object_in_field) { double("presented in field") }
+ let(:field) { type_with_field(custom_type, :read_field, object_in_field).fields["testField"].to_graphql }
+
+ it "checks both field & type permissions" do
+ spy_ability_check_for(:read_field, object_in_field, passed: true)
+ spy_ability_check_for(:read_type, object_in_field, passed: true)
+
+ expect(resolved).to eq(object_in_field)
+ end
+
+ it "returns nil if viewing was not allowed" do
+ spy_ability_check_for(:read_field, object_in_field, passed: false)
+ spy_ability_check_for(:read_type, object_in_field, passed: true)
+
+ expect(resolved).to be_nil
+ end
+
+ context "when the field is a list" do
+ let(:object_1) { double("presented in field 1") }
+ let(:object_2) { double("presented in field 2") }
+ let(:presented_types) { [double(object: object_1), double(object: object_2)] }
+ let(:field) { type_with_field([custom_type], :read_field, presented_types).fields["testField"].to_graphql }
+
+ it "checks all permissions" do
+ allow(Ability).to receive(:allowed?) { true }
+
+ spy_ability_check_for(:read_field, object_1, passed: true)
+ spy_ability_check_for(:read_type, object_1, passed: true)
+ spy_ability_check_for(:read_field, object_2, passed: true)
+ spy_ability_check_for(:read_type, object_2, passed: true)
+
+ expect(resolved).to eq(presented_types)
+ end
+
+ it "filters out objects that the user cannot see" do
+ allow(Ability).to receive(:allowed?) { true }
+
+ spy_ability_check_for(:read_type, object_1, passed: false)
+
+ expect(resolved.map(&:object)).to contain_exactly(object_2)
+ end
+ end
+ end
+ end
+
+ private
+
+ def spy_ability_check_for(ability, object, passed: true)
+ expect(Ability)
+ .to receive(:allowed?)
+ .with(current_user, ability, object)
+ .and_return(passed)
+ end
+end
diff --git a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
index 95bf7685ade..13cf52fd795 100644
--- a/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
+++ b/spec/lib/gitlab/graphql/authorize/authorize_resource_spec.rb
@@ -100,4 +100,22 @@ describe Gitlab::Graphql::Authorize::AuthorizeResource do
expect { fake_class.new.find_object }.to raise_error(/Implement #find_object in #{fake_class.name}/)
end
end
+
+ describe '#authorize' do
+ it 'adds permissions from subclasses to those of superclasses when used on classes' do
+ base_class = Class.new do
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ authorize :base_authorization
+ end
+
+ sub_class = Class.new(base_class) do
+ authorize :sub_authorization
+ end
+
+ expect(base_class.required_permissions).to contain_exactly(:base_authorization)
+ expect(sub_class.required_permissions)
+ .to contain_exactly(:base_authorization, :sub_authorization)
+ end
+ end
end
diff --git a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb b/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb
deleted file mode 100644
index cf3a8bcc8b4..00000000000
--- a/spec/lib/gitlab/graphql/authorize/instrumentation_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Graphql::Authorize::Instrumentation do
- describe '#build_checker' do
- let(:current_user) { double(:current_user) }
- let(:abilities) { [double(:first_ability), double(:last_ability)] }
-
- let(:checker) do
- described_class.new.__send__(:build_checker, current_user, abilities)
- end
-
- it 'returns a checker which checks for a single object' do
- object = double(:object)
-
- abilities.each do |ability|
- spy_ability_check_for(ability, object, passed: true)
- end
-
- expect(checker.call(object)).to eq(object)
- end
-
- it 'returns a checker which checks for all objects' do
- objects = [double(:first), double(:last)]
-
- abilities.each do |ability|
- objects.each do |object|
- spy_ability_check_for(ability, object, passed: true)
- end
- end
-
- expect(checker.call(objects)).to eq(objects)
- end
-
- context 'when some objects would not pass the check' do
- it 'returns nil when it is single object' do
- disallowed = double(:object)
-
- spy_ability_check_for(abilities.first, disallowed, passed: false)
-
- expect(checker.call(disallowed)).to be_nil
- end
-
- it 'returns only objects which passed when there are more than one' do
- allowed = double(:allowed)
- disallowed = double(:disallowed)
-
- spy_ability_check_for(abilities.first, disallowed, passed: false)
-
- abilities.each do |ability|
- spy_ability_check_for(ability, allowed, passed: true)
- end
-
- expect(checker.call([disallowed, allowed]))
- .to contain_exactly(allowed)
- end
- end
-
- def spy_ability_check_for(ability, object, passed: true)
- expect(Ability)
- .to receive(:allowed?)
- .with(current_user, ability, object)
- .and_return(passed)
- end
- end
-end
diff --git a/spec/lib/gitlab/graphql/authorize_spec.rb b/spec/lib/gitlab/graphql/authorize_spec.rb
deleted file mode 100644
index 9c17a3b0e4b..00000000000
--- a/spec/lib/gitlab/graphql/authorize_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Graphql::Authorize do
- describe '#authorize' do
- it 'adds permissions from subclasses to those of superclasses when used on classes' do
- base_class = Class.new do
- extend Gitlab::Graphql::Authorize
-
- authorize :base_authorization
- end
- sub_class = Class.new(base_class) do
- authorize :sub_authorization
- end
-
- expect(base_class.required_permissions).to contain_exactly(:base_authorization)
- expect(sub_class.required_permissions)
- .to contain_exactly(:base_authorization, :sub_authorization)
- end
- end
-end
diff --git a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
index 9bcc1e78a78..fefa2881b18 100644
--- a/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
+++ b/spec/lib/gitlab/graphql/connections/keyset_connection_spec.rb
@@ -85,6 +85,11 @@ describe Gitlab::Graphql::Connections::KeysetConnection do
expect(subject.paged_nodes.size).to eq(3)
end
+ it 'is a loaded memoized array' do
+ expect(subject.paged_nodes).to be_an(Array)
+ expect(subject.paged_nodes.object_id).to eq(subject.paged_nodes.object_id)
+ end
+
context 'when `first` is passed' do
let(:arguments) { { first: 2 } }
diff --git a/spec/lib/gitlab/graphql/generic_tracing_spec.rb b/spec/lib/gitlab/graphql/generic_tracing_spec.rb
new file mode 100644
index 00000000000..ae92dcc40af
--- /dev/null
+++ b/spec/lib/gitlab/graphql/generic_tracing_spec.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::GenericTracing do
+ let(:graphql_duration_seconds_histogram) { double('Gitlab::Metrics::NullMetric') }
+
+ it 'updates graphql histogram with expected labels' do
+ query = 'query { users { id } }'
+ tracer = described_class.new
+
+ allow(tracer)
+ .to receive(:graphql_duration_seconds)
+ .and_return(graphql_duration_seconds_histogram)
+
+ expect_metric('graphql.lex', 'lex')
+ expect_metric('graphql.parse', 'parse')
+ expect_metric('graphql.validate', 'validate')
+ expect_metric('graphql.analyze', 'analyze_multiplex')
+ expect_metric('graphql.execute', 'execute_query_lazy')
+ expect_metric('graphql.execute', 'execute_multiplex')
+
+ GitlabSchema.execute(query, context: { tracers: [tracer] })
+ end
+
+ context "when labkit tracing is enabled" do
+ before do
+ expect(Labkit::Tracing).to receive(:enabled?).and_return(true)
+ end
+
+ it 'yields with labkit tracing' do
+ expected_tags = {
+ 'component' => 'web',
+ 'span.kind' => 'server',
+ 'platform_key' => 'pkey',
+ 'key' => 'key'
+ }
+
+ expect(Labkit::Tracing)
+ .to receive(:with_tracing)
+ .with(operation_name: "pkey.key", tags: expected_tags)
+ .and_yield
+
+ expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control
+ end
+ end
+
+ context "when labkit tracing is disabled" do
+ before do
+ expect(Labkit::Tracing).to receive(:enabled?).and_return(false)
+ end
+
+ it 'yields without measurement' do
+ expect(Labkit::Tracing).not_to receive(:with_tracing)
+
+ expect { |b| described_class.new.platform_trace('pkey', 'key', nil, &b) }.to yield_control
+ end
+ end
+
+ private
+
+ def expect_metric(platform_key, key)
+ expect(graphql_duration_seconds_histogram)
+ .to receive(:observe)
+ .with({ platform_key: platform_key, key: key }, be > 0.0)
+ end
+end
diff --git a/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb
new file mode 100644
index 00000000000..ec2fcad31e5
--- /dev/null
+++ b/spec/lib/gitlab/graphql/loaders/batch_project_statistics_loader_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Loaders::BatchProjectStatisticsLoader do
+ describe '#find' do
+ it 'only queries once for project statistics' do
+ stats = create_list(:project_statistics, 2)
+ project1 = stats.first.project
+ project2 = stats.last.project
+
+ expect do
+ described_class.new(project1.id).find
+ described_class.new(project2.id).find
+ end.not_to exceed_query_limit(1)
+ end
+ 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
new file mode 100644
index 00000000000..66033736e01
--- /dev/null
+++ b/spec/lib/gitlab/graphql/query_analyzers/logger_analyzer_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer do
+ subject { described_class.new }
+
+ describe '#analyze?' do
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(graphql_logging: false)
+ end
+
+ it 'disables the analyzer' do
+ expect(subject.analyze?(anything)).to be_falsey
+ end
+ end
+
+ context 'feature flag enabled by default' do
+ it 'enables the analyzer' do
+ expect(subject.analyze?(anything)).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb b/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb
new file mode 100644
index 00000000000..d45e690160c
--- /dev/null
+++ b/spec/lib/gitlab/graphql/representation/tree_entry_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Graphql::Representation::TreeEntry do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ describe '.decorate' do
+ it 'returns NilClass when given nil' do
+ expect(described_class.decorate(nil, repository)).to be_nil
+ end
+
+ it 'returns array of TreeEntry' do
+ entries = described_class.decorate(repository.tree.blobs, repository)
+
+ expect(entries.first).to be_a(described_class)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/graphql_logger_spec.rb b/spec/lib/gitlab/graphql_logger_spec.rb
new file mode 100644
index 00000000000..4977f98b83e
--- /dev/null
+++ b/spec/lib/gitlab/graphql_logger_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::GraphqlLogger do
+ subject { described_class.new('/dev/null') }
+
+ let(:now) { Time.now }
+
+ it 'builds a logger once' do
+ expect(::Logger).to receive(:new).and_call_original
+
+ subject.info('hello world')
+ subject.error('hello again')
+ end
+
+ context 'logging a GraphQL query' do
+ let(:query) { File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql')) }
+
+ it 'logs a query from JSON' do
+ analyzer_memo = {
+ query_string: query,
+ variables: {},
+ complexity: 181,
+ depth: 0,
+ duration: 7
+ }
+
+ output = subject.format_message('INFO', now, 'test', analyzer_memo)
+
+ data = JSON.parse(output)
+ expect(data['severity']).to eq('INFO')
+ expect(data['time']).to eq(now.utc.iso8601(3))
+ expect(data['complexity']).to eq(181)
+ expect(data['variables']).to eq({})
+ expect(data['depth']).to eq(0)
+ expect(data['duration']).to eq(7)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/group_search_results_spec.rb b/spec/lib/gitlab/group_search_results_spec.rb
new file mode 100644
index 00000000000..2734fcef0a0
--- /dev/null
+++ b/spec/lib/gitlab/group_search_results_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Gitlab::GroupSearchResults do
+ let(:user) { create(:user) }
+
+ describe 'user search' do
+ let(:group) { create(:group) }
+
+ it 'returns the users belonging to the group matching the search query' do
+ user1 = create(:user, username: 'gob_bluth')
+ create(:group_member, :developer, user: user1, group: group)
+
+ user2 = create(:user, username: 'michael_bluth')
+ create(:group_member, :developer, user: user2, group: group)
+
+ create(:user, username: 'gob_2018')
+
+ result = described_class.new(user, anything, group, 'gob').objects('users')
+
+ expect(result).to eq [user1]
+ end
+
+ it 'returns the user belonging to the subgroup matching the search query', :nested_groups do
+ user1 = create(:user, username: 'gob_bluth')
+ subgroup = create(:group, parent: group)
+ create(:group_member, :developer, user: user1, group: subgroup)
+
+ create(:user, username: 'gob_2018')
+
+ result = described_class.new(user, anything, group, 'gob').objects('users')
+
+ expect(result).to eq [user1]
+ end
+
+ it 'returns the user belonging to the parent group matching the search query', :nested_groups do
+ user1 = create(:user, username: 'gob_bluth')
+ parent_group = create(:group, children: [group])
+ create(:group_member, :developer, user: user1, group: parent_group)
+
+ create(:user, username: 'gob_2018')
+
+ result = described_class.new(user, anything, group, 'gob').objects('users')
+
+ expect(result).to eq [user1]
+ end
+
+ it 'does not return the user belonging to the private subgroup', :nested_groups do
+ user1 = create(:user, username: 'gob_bluth')
+ subgroup = create(:group, :private, parent: group)
+ create(:group_member, :developer, user: user1, group: subgroup)
+
+ create(:user, username: 'gob_2018')
+
+ result = described_class.new(user, anything, group, 'gob').objects('users')
+
+ expect(result).to eq []
+ end
+
+ it 'does not return the user belonging to an unrelated group' do
+ user = create(:user, username: 'gob_bluth')
+ unrelated_group = create(:group)
+ create(:group_member, :developer, user: user, group: unrelated_group)
+
+ result = described_class.new(user, anything, group, 'gob').objects('users')
+
+ expect(result).to eq []
+ end
+ end
+end
diff --git a/spec/lib/gitlab/hashed_storage/migrator_spec.rb b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
index 3942f168ceb..8e253b51597 100644
--- a/spec/lib/gitlab/hashed_storage/migrator_spec.rb
+++ b/spec/lib/gitlab/hashed_storage/migrator_spec.rb
@@ -1,21 +1,31 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe Gitlab::HashedStorage::Migrator do
- describe '#bulk_schedule' do
- it 'schedules job to StorageMigratorWorker' do
+describe Gitlab::HashedStorage::Migrator, :sidekiq, :redis do
+ describe '#bulk_schedule_migration' do
+ it 'schedules job to HashedStorage::MigratorWorker' do
+ Sidekiq::Testing.fake! do
+ expect { subject.bulk_schedule_migration(start: 1, finish: 5) }.to change(HashedStorage::MigratorWorker.jobs, :size).by(1)
+ end
+ end
+ end
+
+ describe '#bulk_schedule_rollback' do
+ it 'schedules job to HashedStorage::RollbackerWorker' do
Sidekiq::Testing.fake! do
- expect { subject.bulk_schedule(start: 1, finish: 5) }.to change(HashedStorage::MigratorWorker.jobs, :size).by(1)
+ expect { subject.bulk_schedule_rollback(start: 1, finish: 5) }.to change(HashedStorage::RollbackerWorker.jobs, :size).by(1)
end
end
end
describe '#bulk_migrate' do
- let(:projects) { create_list(:project, 2, :legacy_storage) }
+ let(:projects) { create_list(:project, 2, :legacy_storage, :empty_repo) }
let(:ids) { projects.map(&:id) }
- it 'enqueue jobs to ProjectMigrateHashedStorageWorker' do
+ it 'enqueue jobs to HashedStorage::ProjectMigrateWorker' do
Sidekiq::Testing.fake! do
- expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(2)
+ expect { subject.bulk_migrate(start: ids.min, finish: ids.max) }.to change(HashedStorage::ProjectMigrateWorker.jobs, :size).by(2)
end
end
@@ -32,13 +42,53 @@ describe Gitlab::HashedStorage::Migrator do
subject.bulk_migrate(start: ids.min, finish: ids.max)
end
- it 'has migrated projects set as writable' do
+ it 'has all projects migrated and set as writable' do
perform_enqueued_jobs do
subject.bulk_migrate(start: ids.min, finish: ids.max)
end
projects.each do |project|
- expect(project.reload.repository_read_only?).to be_falsey
+ project.reload
+
+ expect(project.hashed_storage?(:repository)).to be_truthy
+ expect(project.repository_read_only?).to be_falsey
+ end
+ end
+ end
+
+ describe '#bulk_rollback' do
+ let(:projects) { create_list(:project, 2, :empty_repo) }
+ let(:ids) { projects.map(&:id) }
+
+ it 'enqueue jobs to HashedStorage::ProjectRollbackWorker' do
+ Sidekiq::Testing.fake! do
+ expect { subject.bulk_rollback(start: ids.min, finish: ids.max) }.to change(HashedStorage::ProjectRollbackWorker.jobs, :size).by(2)
+ end
+ end
+
+ it 'rescues and log exceptions' do
+ allow_any_instance_of(Project).to receive(:rollback_to_legacy_storage!).and_raise(StandardError)
+ expect { subject.bulk_rollback(start: ids.min, finish: ids.max) }.not_to raise_error
+ end
+
+ it 'delegates each project in specified range to #rollback' do
+ projects.each do |project|
+ expect(subject).to receive(:rollback).with(project)
+ end
+
+ subject.bulk_rollback(start: ids.min, finish: ids.max)
+ end
+
+ it 'has all projects rolledback and set as writable' do
+ perform_enqueued_jobs do
+ subject.bulk_rollback(start: ids.min, finish: ids.max)
+ end
+
+ projects.each do |project|
+ project.reload
+
+ expect(project.legacy_storage?).to be_truthy
+ expect(project.repository_read_only?).to be_falsey
end
end
end
@@ -48,7 +98,7 @@ describe Gitlab::HashedStorage::Migrator do
it 'enqueues project migration job' do
Sidekiq::Testing.fake! do
- expect { subject.migrate(project) }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1)
+ expect { subject.migrate(project) }.to change(HashedStorage::ProjectMigrateWorker.jobs, :size).by(1)
end
end
@@ -79,7 +129,7 @@ describe Gitlab::HashedStorage::Migrator do
it 'doesnt enqueue any migration job' do
Sidekiq::Testing.fake! do
- expect { subject.migrate(project) }.not_to change(ProjectMigrateHashedStorageWorker.jobs, :size)
+ expect { subject.migrate(project) }.not_to change(HashedStorage::ProjectMigrateWorker.jobs, :size)
end
end
@@ -88,4 +138,98 @@ describe Gitlab::HashedStorage::Migrator do
end
end
end
+
+ describe '#rollback' do
+ let(:project) { create(:project, :empty_repo) }
+
+ it 'enqueues project rollback job' do
+ Sidekiq::Testing.fake! do
+ expect { subject.rollback(project) }.to change(HashedStorage::ProjectRollbackWorker.jobs, :size).by(1)
+ end
+ end
+
+ it 'rescues and log exceptions' do
+ allow(project).to receive(:rollback_to_hashed_storage!).and_raise(StandardError)
+
+ expect { subject.rollback(project) }.not_to raise_error
+ end
+
+ it 'rolls-back project storage' do
+ perform_enqueued_jobs do
+ subject.rollback(project)
+ end
+
+ expect(project.reload.legacy_storage?).to be_truthy
+ end
+
+ it 'has rolled-back project set as writable' do
+ perform_enqueued_jobs do
+ subject.rollback(project)
+ end
+
+ expect(project.reload.repository_read_only?).to be_falsey
+ end
+
+ context 'when project is already on legacy storage' do
+ let(:project) { create(:project, :legacy_storage, :empty_repo) }
+
+ it 'doesnt enqueue any rollback job' do
+ Sidekiq::Testing.fake! do
+ expect { subject.rollback(project) }.not_to change(HashedStorage::ProjectRollbackWorker.jobs, :size)
+ end
+ end
+
+ it 'returns false' do
+ expect(subject.rollback(project)).to be_falsey
+ end
+ end
+ end
+
+ describe 'migration_pending?' do
+ set(:project) { create(:project, :empty_repo) }
+
+ it 'returns true when there are MigratorWorker jobs scheduled' do
+ Sidekiq::Testing.disable! do
+ ::HashedStorage::MigratorWorker.perform_async(1, 5)
+
+ expect(subject.migration_pending?).to be_truthy
+ end
+ end
+
+ it 'returns true when there are ProjectMigrateWorker jobs scheduled' do
+ Sidekiq::Testing.disable! do
+ ::HashedStorage::ProjectMigrateWorker.perform_async(1)
+
+ expect(subject.migration_pending?).to be_truthy
+ end
+ end
+
+ it 'returns false when queues are empty' do
+ expect(subject.migration_pending?).to be_falsey
+ end
+ end
+
+ describe 'rollback_pending?' do
+ set(:project) { create(:project, :empty_repo) }
+
+ it 'returns true when there are RollbackerWorker jobs scheduled' do
+ Sidekiq::Testing.disable! do
+ ::HashedStorage::RollbackerWorker.perform_async(1, 5)
+
+ expect(subject.rollback_pending?).to be_truthy
+ end
+ end
+
+ it 'returns true when there are jobs scheduled' do
+ Sidekiq::Testing.disable! do
+ ::HashedStorage::ProjectRollbackWorker.perform_async(1)
+
+ expect(subject.rollback_pending?).to be_truthy
+ end
+ end
+
+ it 'returns false when queues are empty' do
+ expect(subject.rollback_pending?).to be_falsey
+ end
+ end
end
diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
index 26529c4759d..569d5dcc757 100644
--- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
@@ -97,13 +97,13 @@ describe Gitlab::HookData::IssuableBuilder do
end
context 'merge_request is assigned' do
- let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user]) }
let(:data) { described_class.new(merge_request).build(user: user) }
it 'returns correct hook data' do
expect(data[:object_attributes]['assignee_id']).to eq(user.id)
- expect(data[:assignee]).to eq(user.hook_attrs)
- expect(data).not_to have_key(:assignees)
+ expect(data[:assignees].first).to eq(user.hook_attrs)
+ expect(data).not_to have_key(:assignee)
end
end
end
diff --git a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
index 9ce697adbba..39f80f92fa6 100644
--- a/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/merge_request_builder_spec.rb
@@ -10,6 +10,7 @@ describe Gitlab::HookData::MergeRequestBuilder do
it 'includes safe attribute' do
%w[
assignee_id
+ assignee_ids
author_id
created_at
description
diff --git a/spec/lib/gitlab/http_connection_adapter_spec.rb b/spec/lib/gitlab/http_connection_adapter_spec.rb
new file mode 100644
index 00000000000..930d1f62272
--- /dev/null
+++ b/spec/lib/gitlab/http_connection_adapter_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::HTTPConnectionAdapter do
+ describe '#connection' do
+ context 'when local requests are not allowed' do
+ it 'sets up the connection' do
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+
+ it 'raises error when it is a request to local address' do
+ uri = URI('http://172.16.0.0/12')
+
+ expect { described_class.new(uri).connection }
+ .to raise_error(Gitlab::HTTP::BlockedUrlError,
+ "URL 'http://172.16.0.0/12' is blocked: Requests to the local network are not allowed")
+ end
+
+ it 'raises error when it is a request to localhost address' do
+ uri = URI('http://127.0.0.1')
+
+ expect { described_class.new(uri).connection }
+ .to raise_error(Gitlab::HTTP::BlockedUrlError,
+ "URL 'http://127.0.0.1' is blocked: Requests to localhost are not allowed")
+ end
+
+ context 'when port different from URL scheme is used' do
+ it 'sets up the addr_port accordingly' do
+ uri = URI('https://example.org:8080')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org:8080')
+ expect(connection.port).to eq(8080)
+ end
+ end
+ end
+
+ context 'when DNS rebinding protection is disabled' do
+ it 'sets up the connection' do
+ stub_application_setting(dns_rebinding_protection_enabled: false)
+
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('example.org')
+ expect(connection.hostname_override).to eq(nil)
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+ end
+
+ context 'when http(s) environment variable is set' do
+ it 'sets up the connection' do
+ stub_env('https_proxy' => 'https://my.proxy')
+
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('example.org')
+ expect(connection.hostname_override).to eq(nil)
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+ end
+
+ context 'when local requests are allowed' do
+ it 'sets up the connection' do
+ uri = URI('https://example.org')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('93.184.216.34')
+ expect(connection.hostname_override).to eq('example.org')
+ expect(connection.addr_port).to eq('example.org')
+ expect(connection.port).to eq(443)
+ end
+
+ it 'sets up the connection when it is a local network' do
+ uri = URI('http://172.16.0.0/12')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('172.16.0.0')
+ expect(connection.hostname_override).to be(nil)
+ expect(connection.addr_port).to eq('172.16.0.0')
+ expect(connection.port).to eq(80)
+ end
+
+ it 'sets up the connection when it is localhost' do
+ uri = URI('http://127.0.0.1')
+
+ connection = described_class.new(uri, allow_local_requests: true).connection
+
+ expect(connection).to be_a(Net::HTTP)
+ expect(connection.address).to eq('127.0.0.1')
+ expect(connection.hostname_override).to be(nil)
+ expect(connection.addr_port).to eq('127.0.0.1')
+ expect(connection.port).to eq(80)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index 6c37c157f5d..158f77cab2c 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -1,6 +1,28 @@
require 'spec_helper'
describe Gitlab::HTTP do
+ include StubRequests
+
+ context 'when allow_local_requests' do
+ it 'sends the request to the correct URI' do
+ stub_full_request('https://example.org:8080', ip_address: '8.8.8.8').to_return(status: 200)
+
+ described_class.get('https://example.org:8080', allow_local_requests: false)
+
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.8:8080').once
+ end
+ end
+
+ context 'when not allow_local_requests' do
+ it 'sends the request to the correct URI' do
+ stub_full_request('https://example.org:8080')
+
+ described_class.get('https://example.org:8080', allow_local_requests: true)
+
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.9:8080').once
+ end
+ end
+
describe 'allow_local_requests_from_hooks_and_services is' do
before do
WebMock.stub_request(:get, /.*/).to_return(status: 200, body: 'Success')
@@ -21,6 +43,8 @@ describe Gitlab::HTTP do
context 'if allow_local_requests set to true' do
it 'override the global value and allow requests to localhost or private network' do
+ stub_full_request('http://localhost:3003')
+
expect { described_class.get('http://localhost:3003', allow_local_requests: true) }.not_to raise_error
end
end
@@ -32,6 +56,8 @@ describe Gitlab::HTTP do
end
it 'allow requests to localhost' do
+ stub_full_request('http://localhost:3003')
+
expect { described_class.get('http://localhost:3003') }.not_to raise_error
end
@@ -49,7 +75,7 @@ describe Gitlab::HTTP do
describe 'handle redirect loops' do
before do
- WebMock.stub_request(:any, "http://example.org").to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep"))
+ stub_full_request("http://example.org", method: :any).to_raise(HTTParty::RedirectionTooDeep.new("Redirection Too Deep"))
end
it 'handles GET requests' do
diff --git a/spec/lib/gitlab/import/merge_request_helpers_spec.rb b/spec/lib/gitlab/import/merge_request_helpers_spec.rb
new file mode 100644
index 00000000000..cc0f2baf905
--- /dev/null
+++ b/spec/lib/gitlab/import/merge_request_helpers_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Import::MergeRequestHelpers, type: :helper do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+
+ describe '.create_merge_request_without_hooks' do
+ let(:iid) { 42 }
+
+ let(:attributes) do
+ {
+ iid: iid,
+ title: 'My Pull Request',
+ description: 'This is my pull request',
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'master-42',
+ target_branch: 'master',
+ state: :merged,
+ author_id: user.id,
+ assignee_id: user.id
+ }
+ end
+
+ subject { helper.create_merge_request_without_hooks(project, attributes, iid) }
+
+ context 'when merge request does not exist' do
+ it 'returns a new object' do
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(false)
+ end
+
+ it 'does load all existing objects' do
+ 5.times do |iid|
+ MergeRequest.create!(
+ attributes.merge(iid: iid, source_branch: iid.to_s))
+ end
+
+ # does ensure that we only load object twice
+ # 1. by #insert_and_return_id
+ # 2. by project.merge_requests.find
+ expect_any_instance_of(MergeRequest).to receive(:attributes)
+ .twice.times.and_call_original
+
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(false)
+ end
+ end
+
+ context 'when merge request does exist' do
+ before do
+ MergeRequest.create!(attributes)
+ end
+
+ it 'returns an existing object' do
+ expect(subject.first).not_to be_nil
+ expect(subject.second).to eq(true)
+ end
+ end
+
+ context 'when project is deleted' do
+ before do
+ project.delete
+ end
+
+ it 'returns an existing object' do
+ expect(subject.first).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import/set_async_jid_spec.rb b/spec/lib/gitlab/import/set_async_jid_spec.rb
new file mode 100644
index 00000000000..51397280138
--- /dev/null
+++ b/spec/lib/gitlab/import/set_async_jid_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::Import::SetAsyncJid do
+ describe '.set_jid', :clean_gitlab_redis_shared_state do
+ let(:project) { create(:project, :import_scheduled) }
+
+ it 'sets the JID in Redis' do
+ expect(Gitlab::SidekiqStatus)
+ .to receive(:set)
+ .with("async-import/#{project.id}", StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ .and_call_original
+
+ described_class.set_jid(project)
+ end
+
+ it 'updates the import JID of the project' do
+ described_class.set_jid(project)
+
+ expect(project.import_state.reload.jid).to eq("async-import/#{project.id}")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
index ec17ad8541f..21a227335cd 100644
--- a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
+ include StubRequests
+
let(:example_url) { 'http://www.example.com' }
let(:strategy) { subject.new(url: example_url, http_method: 'post') }
let!(:project) { create(:project, :with_export) }
@@ -32,5 +34,17 @@ describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
strategy.execute(user, project)
end
+
+ context 'when upload fails' do
+ it 'stores the export error' do
+ stub_full_request(example_url, method: :post).to_return(status: [404, 'Page not found'])
+
+ strategy.execute(user, project)
+
+ errors = project.import_export_shared.errors
+ expect(errors).not_to be_empty
+ expect(errors.first).to eq "Error uploading the project. Code 404: Page not found"
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index f2eccec4635..2242543daad 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -99,7 +99,10 @@ merge_requests:
- timelogs
- head_pipeline
- latest_merge_request_diff
-- merge_request_pipelines
+- pipelines_for_merge_request
+- merge_request_assignees
+- suggestions
+- assignees
merge_request_diff:
- merge_request
- merge_request_diff_commits
@@ -127,7 +130,7 @@ ci_pipelines:
- scheduled_actions
- artifacts
- pipeline_schedule
-- merge_requests
+- merge_requests_as_head_pipeline
- merge_request
- deployments
- environments
@@ -220,6 +223,7 @@ project:
- packagist_service
- pivotaltracker_service
- prometheus_service
+- hipchat_service
- flowdock_service
- assembla_service
- asana_service
@@ -233,6 +237,7 @@ project:
- pushover_service
- jira_service
- redmine_service
+- youtrack_service
- custom_issue_tracker_service
- bugzilla_service
- gitlab_issue_tracker_service
@@ -317,6 +322,7 @@ project:
- pool_repository
- kubernetes_namespaces
- error_tracking_setting
+- metrics_setting
award_emoji:
- awardable
- user
@@ -333,6 +339,9 @@ push_event_payload:
issue_assignees:
- issue
- assignee
+merge_request_assignees:
+- merge_request
+- assignee
lfs_file_locks:
- user
project_badges:
@@ -350,3 +359,7 @@ resource_label_events:
- label
error_tracking_setting:
- project
+suggestions:
+- note
+metrics_setting:
+- project
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
index 536cc359d39..99669285d5b 100644
--- a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -18,7 +18,11 @@ describe Gitlab::ImportExport::AttributeCleaner do
'notid' => 99,
'import_source' => 'whatever',
'import_type' => 'whatever',
- 'non_existent_attr' => 'whatever'
+ 'non_existent_attr' => 'whatever',
+ 'some_html' => '<p>dodgy html</p>',
+ 'legit_html' => '<p>legit html</p>',
+ '_html' => '<p>perfectly ordinary html</p>',
+ 'cached_markdown_version' => 12345
}
end
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 87ab81d8169..ddfbb020a55 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -29,7 +29,7 @@ describe 'Import/Export attribute configuration' do
it 'has no new columns' do
relation_names.each do |relation_name|
relation_class = relation_class_for_name(relation_name)
- relation_attributes = relation_class.new.attributes.keys
+ relation_attributes = relation_class.new.attributes.keys - relation_class.encrypted_attributes.keys.map(&:to_s)
current_attributes = parsed_attributes(relation_name, relation_attributes)
safe_attributes = safe_model_attributes[relation_class.to_s].dup || []
diff --git a/spec/lib/gitlab/import_export/members_mapper_spec.rb b/spec/lib/gitlab/import_export/members_mapper_spec.rb
index 67e4c289906..b95b5dfe791 100644
--- a/spec/lib/gitlab/import_export/members_mapper_spec.rb
+++ b/spec/lib/gitlab/import_export/members_mapper_spec.rb
@@ -12,7 +12,6 @@ describe Gitlab::ImportExport::MembersMapper do
"access_level" => 40,
"source_id" => 14,
"source_type" => "Project",
- "user_id" => 19,
"notification_level" => 3,
"created_at" => "2016-03-11T10:21:44.822Z",
"updated_at" => "2016-03-11T10:21:44.822Z",
@@ -25,7 +24,8 @@ describe Gitlab::ImportExport::MembersMapper do
"id" => exported_user_id,
"email" => user2.email,
"username" => 'test'
- }
+ },
+ "user_id" => 19
},
{
"id" => 3,
@@ -73,6 +73,22 @@ describe Gitlab::ImportExport::MembersMapper do
expect(user2.authorized_project?(project)).to be true
end
+ it 'maps an owner as a maintainer' do
+ exported_members.first['access_level'] = ProjectMember::OWNER
+
+ expect(members_mapper.map[exported_user_id]).to eq(user2.id)
+ expect(ProjectMember.find_by_user_id(user2.id).access_level).to eq(ProjectMember::MAINTAINER)
+ end
+
+ it 'removes old user_id from member_hash to avoid conflict with user key' do
+ expect(ProjectMember).to receive(:create)
+ .twice
+ .with(hash_excluding('user_id'))
+ .and_call_original
+
+ members_mapper.map
+ end
+
context 'user is not an admin' do
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
index 68eaa70e6b6..4b234411a44 100644
--- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
+++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb
@@ -41,4 +41,20 @@ describe Gitlab::ImportExport::MergeRequestParser do
expect(parsed_merge_request).to eq(merge_request)
end
+
+ context 'when the merge request has diffs' do
+ let(:merge_request) do
+ build(:merge_request, source_project: forked_project, target_project: project)
+ end
+
+ context 'when the diff is invalid' do
+ let(:merge_request_diff) { build(:merge_request_diff, merge_request: merge_request, base_commit_sha: 'foobar') }
+
+ it 'sets the diff to nil' do
+ expect(merge_request_diff).to be_invalid
+ expect(merge_request_diff.merge_request).to eq merge_request
+ expect(parsed_merge_request.merge_request_diff).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 1327f414498..fb7bddb386c 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -158,6 +158,8 @@
{
"id": 351,
"note": "Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.",
+ "note_html": "<p>something else entirely</p>",
+ "cached_markdown_version": 917504,
"noteable_type": "Issue",
"author_id": 26,
"created_at": "2016-06-14T15:02:47.770Z",
@@ -2363,6 +2365,8 @@
{
"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",
@@ -6630,6 +6634,26 @@
"deploy_keys": [],
"services": [
{
+ "id": 101,
+ "title": "YouTrack",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.327Z",
+ "updated_at": "2016-06-14T15:01:51.327Z",
+ "active": false,
+ "properties": {},
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "job_events": true,
+ "type": "YoutrackService",
+ "category": "issue_tracker",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
"id": 100,
"title": "JetBrains TeamCity CI",
"project_id": 5,
@@ -6775,6 +6799,28 @@
"wiki_page_events": true
},
{
+ "id": 93,
+ "title": "HipChat",
+ "project_id": 5,
+ "created_at": "2016-06-14T15:01:51.219Z",
+ "updated_at": "2016-06-14T15:01:51.219Z",
+ "active": false,
+ "properties": {
+ "notify_only_broken_pipelines": true
+ },
+ "template": false,
+ "push_events": true,
+ "issues_events": true,
+ "merge_requests_events": true,
+ "tag_push_events": true,
+ "note_events": true,
+ "pipeline_events": true,
+ "type": "HipchatService",
+ "category": "common",
+ "default": false,
+ "wiki_page_events": true
+ },
+ {
"id": 91,
"title": "Flowdock",
"project_id": 5,
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 6084dc96410..ca46006ea58 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -58,6 +58,26 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
end
+ context 'when importing a project with cached_markdown_version and note_html' do
+ context 'for an Issue' do
+ it 'does not import note_html' do
+ note_content = 'Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi'
+ issue_note = Issue.find_by(description: 'Aliquam enim illo et possimus.').notes.select { |n| n.note.match(/#{note_content}/)}.first
+
+ expect(issue_note.note_html).to match(/#{note_content}/)
+ end
+ end
+
+ context 'for a Merge Request' do
+ it 'does not import note_html' do
+ note_content = 'Sit voluptatibus eveniet architecto quidem'
+ merge_request_note = MergeRequest.find_by(title: 'MR1').notes.select { |n| n.note.match(/#{note_content}/)}.first
+
+ expect(merge_request_note.note_html).to match(/#{note_content}/)
+ end
+ end
+ end
+
it 'creates a valid pipeline note' do
expect(Ci::Pipeline.find_by_sha('sha-notes').notes).not_to be_empty
end
@@ -328,6 +348,19 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end
context 'when the project has overridden params in import data' do
+ it 'handles string versions of visibility_level' do
+ # Project needs to be in a group for visibility level comparison
+ # to happen
+ group = create(:group)
+ project.group = group
+
+ project.create_import_data(data: { override_params: { visibility_level: Gitlab::VisibilityLevel::INTERNAL.to_s } })
+
+ restored_project_json
+
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+
it 'overwrites the params stored in the JSON' do
project.create_import_data(data: { override_params: { description: "Overridden" } })
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 cfc3e0ce926..bc4f867e891 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -91,7 +91,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
end
it 'has issue comments' do
- expect(saved_project_json['issues'].first['notes']).not_to be_empty
+ notes = saved_project_json['issues'].first['notes']
+
+ expect(notes).not_to be_empty
+ expect(notes.first['type']).to eq('DiscussionNote')
end
it 'has issue assignees' do
@@ -299,7 +302,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
create(:commit_status, project: project, pipeline: ci_build.pipeline)
create(:milestone, project: project)
- create(:note, noteable: issue, project: project)
+ create(:discussion_note, noteable: issue, project: project)
create(:note, noteable: merge_request, project: project)
create(:note, noteable: snippet, project: project)
create(:note_on_commit,
diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
index 8a699eb1461..e2ffb2adb9b 100644
--- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb
@@ -34,11 +34,5 @@ describe Gitlab::ImportExport::RepoRestorer do
it 'restores the repo successfully' do
expect(restorer.restore).to be_truthy
end
-
- it 'has the webhooks' do
- restorer.restore
-
- expect(project_hook_exists?(project)).to be true
- end
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 baca8f6d542..9093d21647a 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -11,6 +11,7 @@ Issue:
- branch_name
- description
- state
+- state_id
- iid
- updated_by_id
- confidential
@@ -158,6 +159,7 @@ MergeRequest:
- created_at
- updated_at
- state
+- state_id
- merge_status
- target_project_id
- iid
@@ -235,6 +237,8 @@ Ci::Pipeline:
- ref
- sha
- before_sha
+- source_sha
+- target_sha
- push_data
- created_at
- updated_at
@@ -419,6 +423,7 @@ Service:
- wiki_page_events
- confidential_issues_events
- confidential_note_events
+- deployment_events
ProjectHook:
- id
- url
@@ -492,6 +497,7 @@ Project:
- merge_requests_ff_only_enabled
- merge_requests_rebase_enabled
- jobs_cache_index
+- external_authorization_classification_label
- pages_https_only
Author:
- name
@@ -601,8 +607,27 @@ ResourceLabelEvent:
- user_id
- created_at
ErrorTracking::ProjectErrorTrackingSetting:
-- id
- api_url
- project_id
- project_name
- organization_name
+Suggestion:
+- id
+- note_id
+- relative_order
+- applied
+- commit_id
+- from_content
+- to_content
+- outdated
+- lines_above
+- lines_below
+MergeRequestAssignee:
+- id
+- user_id
+- merge_request_id
+ProjectMetricsSetting:
+- project_id
+- external_dashboard_url
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/import_sources_spec.rb b/spec/lib/gitlab/import_sources_spec.rb
index 94abf9679c4..8060b5d4448 100644
--- a/spec/lib/gitlab/import_sources_spec.rb
+++ b/spec/lib/gitlab/import_sources_spec.rb
@@ -14,7 +14,8 @@ describe Gitlab::ImportSources do
'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project',
'Gitea' => 'gitea',
- 'Manifest file' => 'manifest'
+ 'Manifest file' => 'manifest',
+ 'Phabricator' => 'phabricator'
}
expect(described_class.options).to eq(expected)
@@ -35,6 +36,7 @@ describe Gitlab::ImportSources do
gitlab_project
gitea
manifest
+ phabricator
)
expect(described_class.values).to eq(expected)
@@ -53,6 +55,7 @@ describe Gitlab::ImportSources do
fogbugz
gitlab_project
gitea
+ phabricator
)
expect(described_class.importer_names).to eq(expected)
@@ -70,7 +73,8 @@ describe Gitlab::ImportSources do
'git' => nil,
'gitlab_project' => Gitlab::ImportExport::Importer,
'gitea' => Gitlab::LegacyGithubImport::Importer,
- 'manifest' => nil
+ 'manifest' => nil,
+ 'phabricator' => Gitlab::PhabricatorImport::Importer
}
import_sources.each do |name, klass|
@@ -91,7 +95,8 @@ describe Gitlab::ImportSources do
'git' => 'Repo by URL',
'gitlab_project' => 'GitLab export',
'gitea' => 'Gitea',
- 'manifest' => 'Manifest file'
+ 'manifest' => 'Manifest file',
+ 'phabricator' => 'Phabricator'
}
import_sources.each do |name, title|
@@ -102,7 +107,7 @@ describe Gitlab::ImportSources do
end
describe 'imports_repository? checker' do
- let(:allowed_importers) { %w[github gitlab_project bitbucket_server] }
+ let(:allowed_importers) { %w[github gitlab_project bitbucket_server phabricator] }
it 'fails if any importer other than the allowed ones implements this method' do
current_importers = described_class.values.select { |kind| described_class.importer(kind).try(:imports_repository?) }
diff --git a/spec/lib/gitlab/issuable_metadata_spec.rb b/spec/lib/gitlab/issuable_metadata_spec.rb
index 42635a68ee1..916f3876a8e 100644
--- a/spec/lib/gitlab/issuable_metadata_spec.rb
+++ b/spec/lib/gitlab/issuable_metadata_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::IssuableMetadata do
let!(:closed_issue) { create(:issue, state: :closed, author: user, project: project) }
let!(:downvote) { create(:award_emoji, :downvote, awardable: closed_issue) }
let!(:upvote) { create(:award_emoji, :upvote, awardable: issue) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") }
let!(:closing_issues) { create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request) }
it 'aggregates stats on issues' do
@@ -28,18 +28,18 @@ describe Gitlab::IssuableMetadata do
expect(data.count).to eq(2)
expect(data[issue.id].upvotes).to eq(1)
expect(data[issue.id].downvotes).to eq(0)
- expect(data[issue.id].notes_count).to eq(0)
+ expect(data[issue.id].user_notes_count).to eq(0)
expect(data[issue.id].merge_requests_count).to eq(1)
expect(data[closed_issue.id].upvotes).to eq(0)
expect(data[closed_issue.id].downvotes).to eq(1)
- expect(data[closed_issue.id].notes_count).to eq(0)
+ expect(data[closed_issue.id].user_notes_count).to eq(0)
expect(data[closed_issue.id].merge_requests_count).to eq(0)
end
end
context 'merge requests' do
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project, target_project: project, title: "Test") }
let!(:merge_request_closed) { create(:merge_request, state: "closed", source_project: project, target_project: project, title: "Closed Test") }
let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) }
let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) }
@@ -51,12 +51,12 @@ describe Gitlab::IssuableMetadata do
expect(data.count).to eq(2)
expect(data[merge_request.id].upvotes).to eq(1)
expect(data[merge_request.id].downvotes).to eq(1)
- expect(data[merge_request.id].notes_count).to eq(1)
+ expect(data[merge_request.id].user_notes_count).to eq(1)
expect(data[merge_request.id].merge_requests_count).to eq(0)
expect(data[merge_request_closed.id].upvotes).to eq(0)
expect(data[merge_request_closed.id].downvotes).to eq(0)
- expect(data[merge_request_closed.id].notes_count).to eq(0)
+ expect(data[merge_request_closed.id].user_notes_count).to eq(0)
expect(data[merge_request_closed.id].merge_requests_count).to eq(0)
end
end
diff --git a/spec/lib/gitlab/json_cache_spec.rb b/spec/lib/gitlab/json_cache_spec.rb
index b52078e8556..b82c09af306 100644
--- a/spec/lib/gitlab/json_cache_spec.rb
+++ b/spec/lib/gitlab/json_cache_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::JsonCache do
let(:namespace) { 'geo' }
let(:key) { 'foo' }
let(:expanded_key) { "#{namespace}:#{key}:#{Rails.version}" }
- let(:broadcast_message) { create(:broadcast_message) }
+ set(:broadcast_message) { create(:broadcast_message) }
subject(:cache) { described_class.new(namespace: namespace, backend: backend) }
@@ -146,6 +146,18 @@ describe Gitlab::JsonCache do
expect(cache.read(key, BroadcastMessage)).to be_nil
end
+
+ it 'gracefully handles excluded fields from attributes during serialization' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(broadcast_message.attributes.except("message_html").to_json)
+
+ result = cache.read(key, BroadcastMessage)
+
+ BroadcastMessage.cached_markdown_fields.html_fields.each do |field|
+ expect(result.public_send(field)).to be_nil
+ end
+ end
end
context 'when the cached value is an array' do
@@ -297,13 +309,79 @@ describe Gitlab::JsonCache do
expect(result).to eq(broadcast_message)
end
+ context 'when the cached value is an instance of ActiveRecord::Base' do
+ it 'returns a persisted record when id is set' do
+ backend.write(expanded_key, broadcast_message.to_json)
+
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to be_persisted
+ end
+
+ it 'returns a new record when id is nil' do
+ backend.write(expanded_key, build(:broadcast_message).to_json)
+
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to be_new_record
+ end
+
+ it 'returns a new record when id is missing' do
+ backend.write(expanded_key, build(:broadcast_message).attributes.except('id').to_json)
+
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to be_new_record
+ end
+
+ it 'gracefully handles bad cached entry' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return('{')
+
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to eq 'block result'
+ end
+
+ it 'gracefully handles an empty hash' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return('{}')
+
+ expect(cache.fetch(key, as: BroadcastMessage)).to be_a(BroadcastMessage)
+ end
+
+ it 'gracefully handles unknown attributes' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(broadcast_message.attributes.merge(unknown_attribute: 1).to_json)
+
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ expect(result).to eq 'block result'
+ end
+
+ it 'gracefully handles excluded fields from attributes during serialization' do
+ allow(backend).to receive(:read)
+ .with(expanded_key)
+ .and_return(broadcast_message.attributes.except("message_html").to_json)
+
+ result = cache.fetch(key, as: BroadcastMessage) { 'block result' }
+
+ BroadcastMessage.cached_markdown_fields.html_fields.each do |field|
+ expect(result.public_send(field)).to be_nil
+ end
+ end
+ end
+
it "returns the result of the block when 'as' option is nil" do
result = cache.fetch(key, as: nil) { 'block result' }
expect(result).to eq('block result')
end
- it "returns the result of the block when 'as' option is not informed" do
+ it "returns the result of the block when 'as' option is missing" do
result = cache.fetch(key) { 'block result' }
expect(result).to eq('block result')
diff --git a/spec/lib/gitlab/json_logger_spec.rb b/spec/lib/gitlab/json_logger_spec.rb
index cff7dd58c8c..d3d9fe9948a 100644
--- a/spec/lib/gitlab/json_logger_spec.rb
+++ b/spec/lib/gitlab/json_logger_spec.rb
@@ -8,7 +8,7 @@ describe Gitlab::JsonLogger do
describe '#format_message' do
before do
- allow(Gitlab::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('new-correlation-id')
end
it 'formats strings' do
diff --git a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb
index 4a669408025..e1106f7496a 100644
--- a/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb
+++ b/spec/lib/gitlab/kubernetes/cluster_role_binding_spec.rb
@@ -28,7 +28,7 @@ describe Gitlab::Kubernetes::ClusterRoleBinding do
subject { cluster_role_binding.generate }
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb
index fe65d03875f..911d6024804 100644
--- a/spec/lib/gitlab/kubernetes/config_map_spec.rb
+++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb
@@ -18,7 +18,7 @@ describe Gitlab::Kubernetes::ConfigMap do
let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: application.files) }
subject { config_map.generate }
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 8433d40b2ea..0de809833e6 100644
--- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -33,6 +33,52 @@ describe Gitlab::Kubernetes::Helm::Api do
end
end
+ describe '#uninstall' do
+ before do
+ allow(client).to receive(:create_pod).and_return(nil)
+ allow(client).to receive(:get_config_map).and_return(nil)
+ allow(client).to receive(:create_config_map).and_return(nil)
+ allow(client).to receive(:delete_pod).and_return(nil)
+ allow(namespace).to receive(:ensure_exists!).once
+ end
+
+ it 'ensures the namespace exists before creating the POD' do
+ expect(namespace).to receive(:ensure_exists!).once.ordered
+ expect(client).to receive(:create_pod).once.ordered
+
+ subject.uninstall(command)
+ end
+
+ it 'removes an existing pod before installing' do
+ expect(client).to receive(:delete_pod).with('install-app-name', 'gitlab-managed-apps').once.ordered
+ expect(client).to receive(:create_pod).once.ordered
+
+ subject.uninstall(command)
+ end
+
+ context 'with a ConfigMap' do
+ let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application_name, files).generate }
+
+ it 'creates a ConfigMap on kubeclient' do
+ expect(client).to receive(:create_config_map).with(resource).once
+
+ subject.install(command)
+ end
+
+ context 'config map already exists' do
+ before do
+ expect(client).to receive(:get_config_map).with("values-content-configuration-#{application_name}", gitlab_namespace).and_return(resource)
+ end
+
+ it 'updates the config map' do
+ expect(client).to receive(:update_config_map).with(resource).once
+
+ subject.install(command)
+ end
+ end
+ end
+ end
+
describe '#install' do
before do
allow(client).to receive(:create_pod).and_return(nil)
diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
index aacae78be43..78a4eb44e38 100644
--- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do
describe '#pod_resource' do
subject { base_command.pod_resource }
- it 'should returns a kubeclient resoure with pod content for application' do
+ it 'returns a kubeclient resoure with pod content for application' do
is_expected.to be_an_instance_of ::Kubeclient::Resource
end
diff --git a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb
index 167bee22fc3..04649353976 100644
--- a/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/certificate_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::Kubernetes::Helm::Certificate do
describe '.generate_root' do
subject { described_class.generate_root }
- it 'should generate a root CA that expires a long way in the future' do
+ it 'generates a root CA that expires a long way in the future' do
expect(subject.cert.not_after).to be > 999.years.from_now
end
end
@@ -13,14 +13,14 @@ describe Gitlab::Kubernetes::Helm::Certificate do
describe '#issue' do
subject { described_class.generate_root.issue }
- it 'should generate a cert that expires soon' do
+ it 'generates a cert that expires soon' do
expect(subject.cert.not_after).to be < 60.minutes.from_now
end
context 'passing in INFINITE_EXPIRY' do
subject { described_class.generate_root.issue(expires_in: described_class::INFINITE_EXPIRY) }
- it 'should generate a cert that expires a long way in the future' do
+ it 'generates a cert that expires a long way in the future' do
expect(subject.cert.not_after).to be > 999.years.from_now
end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
new file mode 100644
index 00000000000..cae92305b19
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::Helm::DeleteCommand do
+ let(:app_name) { 'app-name' }
+ let(:rbac) { true }
+ let(:files) { {} }
+ let(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) }
+
+ subject { delete_command }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ helm delete --purge app-name
+ EOS
+ end
+ end
+
+ context 'when there is a ca.pem file' do
+ let(:files) { { 'ca.pem': 'some file content' } }
+
+ it_behaves_like 'helm commands' do
+ let(:commands) do
+ <<~EOS
+ helm init --upgrade
+ for i in $(seq 1 30); do helm version && break; sleep 1s; echo "Retrying ($i)..."; done
+ #{helm_delete_command}
+ EOS
+ end
+
+ let(:helm_delete_command) do
+ <<~EOS.squish
+ helm delete --purge app-name
+ --tls
+ --tls-ca-cert /data/helm/app-name/config/ca.pem
+ --tls-cert /data/helm/app-name/config/cert.pem
+ --tls-key /data/helm/app-name/config/key.pem
+ EOS
+ end
+ end
+ end
+
+ describe '#pod_resource' do
+ subject { delete_command.pod_resource }
+
+ context 'rbac is enabled' do
+ let(:rbac) { true }
+
+ it 'generates a pod that uses the tiller serviceAccountName' do
+ expect(subject.spec.serviceAccountName).to eq('tiller')
+ end
+ end
+
+ context 'rbac is not enabled' do
+ let(:rbac) { false }
+
+ it 'generates a pod that uses the default serviceAccountName' do
+ expect(subject.spec.serviceAcccountName).to be_nil
+ end
+ end
+ end
+
+ describe '#pod_name' do
+ subject { delete_command.pod_name }
+
+ it { is_expected.to eq('uninstall-app-name') }
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index 95b6b3fd953..06c8d127951 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -10,11 +10,11 @@ describe Gitlab::Kubernetes::Helm::Pod do
subject { described_class.new(command, namespace, service_account_name: service_account_name) }
context 'with a command' do
- it 'should generate a Kubeclient::Resource' do
+ it 'generates a Kubeclient::Resource' do
expect(subject.generate).to be_a_kind_of(Kubeclient::Resource)
end
- it 'should generate the appropriate metadata' do
+ it 'generates the appropriate metadata' do
metadata = subject.generate.metadata
expect(metadata.name).to eq("install-#{app.name}")
expect(metadata.namespace).to eq('gitlab-managed-apps')
@@ -22,12 +22,12 @@ describe Gitlab::Kubernetes::Helm::Pod do
expect(metadata.labels['gitlab.org/application']).to eq(app.name)
end
- it 'should generate a container spec' do
+ it 'generates a container spec' do
spec = subject.generate.spec
expect(spec.containers.count).to eq(1)
end
- it 'should generate the appropriate specifications for the container' do
+ it 'generates the appropriate specifications for the container' do
container = subject.generate.spec.containers.first
expect(container.name).to eq('helm')
expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7')
@@ -37,30 +37,30 @@ describe Gitlab::Kubernetes::Helm::Pod do
expect(container.args).to match_array(["-c", "$(COMMAND_SCRIPT)"])
end
- it 'should include a never restart policy' do
+ it 'includes a never restart policy' do
spec = subject.generate.spec
expect(spec.restartPolicy).to eq('Never')
end
- it 'should include volumes for the container' do
+ it 'includes volumes for the container' do
container = subject.generate.spec.containers.first
expect(container.volumeMounts.first['name']).to eq('configuration-volume')
expect(container.volumeMounts.first['mountPath']).to eq("/data/helm/#{app.name}/config")
end
- it 'should include a volume inside the specification' do
+ it 'includes a volume inside the specification' do
spec = subject.generate.spec
expect(spec.volumes.first['name']).to eq('configuration-volume')
end
- it 'should mount configMap specification in the volume' do
+ it 'mounts configMap specification in the volume' do
volume = subject.generate.spec.volumes.first
expect(volume.configMap['name']).to eq("values-content-configuration-#{app.name}")
expect(volume.configMap['items'].first['key']).to eq(:'values.yaml')
expect(volume.configMap['items'].first['path']).to eq(:'values.yaml')
end
- it 'should have no serviceAccountName' do
+ it 'has no serviceAccountName' do
spec = subject.generate.spec
expect(spec.serviceAccountName).to be_nil
end
@@ -68,7 +68,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
context 'with a service_account_name' do
let(:service_account_name) { 'sa' }
- it 'should use the serviceAccountName provided' do
+ it 'uses the serviceAccountName provided' do
spec = subject.generate.spec
expect(spec.serviceAccountName).to eq(service_account_name)
end
diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
index 02364e92149..978e64c4407 100644
--- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb
+++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb
@@ -50,6 +50,36 @@ describe Gitlab::Kubernetes::KubeClient do
end
end
+ describe '#initialize' do
+ shared_examples 'local address' do
+ it 'blocks local addresses' do
+ expect { client }.to raise_error(Gitlab::UrlBlocker::BlockedUrlError)
+ end
+
+ context 'when local requests are allowed' do
+ before do
+ stub_application_setting(allow_local_requests_from_hooks_and_services: true)
+ end
+
+ it 'allows local addresses' do
+ expect { client }.not_to raise_error
+ end
+ end
+ end
+
+ context 'localhost address' do
+ let(:api_url) { 'http://localhost:22' }
+
+ it_behaves_like 'local address'
+ end
+
+ context 'private network address' do
+ let(:api_url) { 'http://192.168.1.2:3003' }
+
+ it_behaves_like 'local address'
+ end
+ end
+
describe '#core_client' do
subject { client.core_client }
diff --git a/spec/lib/gitlab/kubernetes/namespace_spec.rb b/spec/lib/gitlab/kubernetes/namespace_spec.rb
index e1c35c355f4..e91a755aa03 100644
--- a/spec/lib/gitlab/kubernetes/namespace_spec.rb
+++ b/spec/lib/gitlab/kubernetes/namespace_spec.rb
@@ -62,5 +62,32 @@ describe Gitlab::Kubernetes::Namespace do
subject.ensure_exists!
end
+
+ context 'when client errors' do
+ let(:exception) { Kubeclient::HttpError.new(500, 'system failure', nil) }
+
+ before do
+ allow(client).to receive(:get_namespace).with(name).once.and_raise(exception)
+ end
+
+ it 'raises the exception' do
+ expect { subject.ensure_exists! }.to raise_error(exception)
+ end
+
+ it 'logs the error' do
+ expect(subject.send(:logger)).to receive(:error).with(
+ hash_including(
+ exception: 'Kubeclient::HttpError',
+ status_code: 500,
+ namespace: 'a_namespace',
+ class_name: 'Gitlab::Kubernetes::Namespace',
+ event: :failed_to_create_namespace,
+ message: 'system failure'
+ )
+ )
+
+ expect { subject.ensure_exists! }.to raise_error(exception)
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/kubernetes/role_binding_spec.rb b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
index a1a59533bfb..50acee254cb 100644
--- a/spec/lib/gitlab/kubernetes/role_binding_spec.rb
+++ b/spec/lib/gitlab/kubernetes/role_binding_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::Kubernetes::RoleBinding, '#generate' do
).generate
end
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes/service_account_spec.rb b/spec/lib/gitlab/kubernetes/service_account_spec.rb
index 8da9e932dc3..0d525966d18 100644
--- a/spec/lib/gitlab/kubernetes/service_account_spec.rb
+++ b/spec/lib/gitlab/kubernetes/service_account_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Kubernetes::ServiceAccount do
subject { service_account.generate }
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb
index 0773d3d9aec..0d334bed45f 100644
--- a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb
+++ b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb
@@ -28,7 +28,7 @@ describe Gitlab::Kubernetes::ServiceAccountToken do
subject { service_account_token.generate }
- it 'should build a Kubeclient Resource' do
+ it 'builds a Kubeclient Resource' do
is_expected.to eq(resource)
end
end
diff --git a/spec/lib/gitlab/kubernetes_spec.rb b/spec/lib/gitlab/kubernetes_spec.rb
index f326d57e9c6..57b570a9166 100644
--- a/spec/lib/gitlab/kubernetes_spec.rb
+++ b/spec/lib/gitlab/kubernetes_spec.rb
@@ -40,10 +40,40 @@ describe Gitlab::Kubernetes do
describe '#filter_by_label' do
it 'returns matching labels' do
- matching_items = [kube_pod(app: 'foo')]
+ matching_items = [kube_pod(track: 'foo'), kube_deployment(track: 'foo')]
+ items = matching_items + [kube_pod, kube_deployment]
+
+ expect(filter_by_label(items, 'track' => 'foo')).to eq(matching_items)
+ end
+ end
+
+ describe '#filter_by_annotation' do
+ it 'returns matching labels' do
+ matching_items = [kube_pod(environment_slug: 'foo'), kube_deployment(environment_slug: 'foo')]
+ items = matching_items + [kube_pod, kube_deployment]
+
+ expect(filter_by_annotation(items, 'app.gitlab.com/env' => 'foo')).to eq(matching_items)
+ end
+ end
+
+ describe '#filter_by_project_environment' do
+ let(:matching_pod) { kube_pod(environment_slug: 'production', project_slug: 'my-cool-app') }
+
+ it 'returns matching legacy env label' do
+ matching_pod['metadata']['annotations'].delete('app.gitlab.com/app')
+ matching_pod['metadata']['annotations'].delete('app.gitlab.com/env')
+ matching_pod['metadata']['labels']['app'] = 'production'
+ matching_items = [matching_pod]
+ items = matching_items + [kube_pod]
+
+ expect(filter_by_project_environment(items, 'my-cool-app', 'production')).to eq(matching_items)
+ end
+
+ it 'returns matching env label' do
+ matching_items = [matching_pod]
items = matching_items + [kube_pod]
- expect(filter_by_label(items, app: 'foo')).to eq(matching_items)
+ expect(filter_by_project_environment(items, 'my-cool-app', 'production')).to eq(matching_items)
end
end
diff --git a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
index 3d4240fa4ba..8675d8691c8 100644
--- a/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/project_creator_spec.rb
@@ -47,12 +47,22 @@ describe Gitlab::LegacyGithubImport::ProjectCreator do
end
context 'when GitHub project is public' do
- it 'sets project visibility to public' do
+ it 'sets project visibility to namespace visibility level' do
repo.private = false
-
project = service.execute
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ expect(project.visibility_level).to eq(namespace.visibility_level)
+ end
+
+ context 'when importing into a user namespace' do
+ subject(:service) { described_class.new(repo, repo.name, user.namespace, user, github_access_token: 'asdffg') }
+
+ it 'sets project visibility to user namespace visibility level' do
+ repo.private = false
+ project = service.execute
+
+ expect(project.visibility_level).to eq(user.namespace.visibility_level)
+ end
end
end
diff --git a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
index 082e3b36dd0..c57b96fb00d 100644
--- a/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/release_formatter_spec.rb
@@ -25,6 +25,7 @@ describe Gitlab::LegacyGithubImport::ReleaseFormatter do
expected = {
project: project,
tag: 'v1.0.0',
+ name: 'First release',
description: 'Release v1.0.0',
created_at: created_at,
updated_at: created_at
diff --git a/spec/lib/gitlab/lets_encrypt/challenge_spec.rb b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
new file mode 100644
index 00000000000..fcd92586362
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Challenge do
+ include LetsEncryptHelpers
+
+ let(:challenge) { described_class.new(acme_challenge_double) }
+
+ LetsEncryptHelpers::ACME_CHALLENGE_METHODS.each do |method, value|
+ describe "##{method}" do
+ it 'delegates to Acme::Client::Resources::Challenge' do
+ expect(challenge.public_send(method)).to eq(value)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb
new file mode 100644
index 00000000000..5454d9c1af4
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Client do
+ include LetsEncryptHelpers
+
+ let(:client) { described_class.new }
+
+ before do
+ stub_application_setting(
+ lets_encrypt_notification_email: 'myemail@test.example.com',
+ lets_encrypt_terms_of_service_accepted: true
+ )
+ end
+
+ let!(:stub_client) { stub_lets_encrypt_client }
+
+ shared_examples 'ensures account registration' do
+ it 'ensures account registration' do
+ subject
+
+ expect(stub_client).to have_received(:new_account).with(
+ contact: 'mailto:myemail@test.example.com',
+ terms_of_service_agreed: true
+ )
+ end
+
+ it 'generates and stores private key and initialize acme client with it' do
+ expect(Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(nil)
+
+ subject
+
+ saved_private_key = Gitlab::CurrentSettings.lets_encrypt_private_key
+
+ expect(saved_private_key).to be
+ expect(Acme::Client).to have_received(:new).with(
+ hash_including(private_key: eq_pem(saved_private_key))
+ )
+ end
+
+ context 'when private key is saved in settings' do
+ let!(:saved_private_key) do
+ key = OpenSSL::PKey::RSA.new(4096).to_pem
+ Gitlab::CurrentSettings.current_application_settings.update(lets_encrypt_private_key: key)
+ key
+ end
+
+ it 'uses current value of private key' do
+ subject
+
+ expect(Acme::Client).to have_received(:new).with(
+ hash_including(private_key: eq_pem(saved_private_key))
+ )
+ expect(Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(saved_private_key)
+ end
+ end
+
+ context 'when acme integration is disabled' do
+ before do
+ stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+ end
+
+ it 'raises error' do
+ expect do
+ subject
+ end.to raise_error('Acme integration is disabled')
+ end
+ end
+ end
+
+ describe '#new_order' do
+ subject(:new_order) { client.new_order('example.com') }
+
+ before do
+ order_double = instance_double('Acme::Order')
+ allow(stub_client).to receive(:new_order).and_return(order_double)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'returns order' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Order)
+ end
+ end
+
+ describe '#load_order' do
+ let(:url) { 'https://example.com/order' }
+ subject { client.load_order(url) }
+
+ before do
+ acme_order = instance_double('Acme::Client::Resources::Order')
+ allow(stub_client).to receive(:order).with(url: url).and_return(acme_order)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'loads order' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Order)
+ end
+ end
+
+ describe '#load_challenge' do
+ let(:url) { 'https://example.com/challenge' }
+ subject { client.load_challenge(url) }
+
+ before do
+ acme_challenge = instance_double('Acme::Client::Resources::Challenge')
+ allow(stub_client).to receive(:challenge).with(url: url).and_return(acme_challenge)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'loads challenge' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Challenge)
+ end
+ end
+
+ describe '#enabled?' do
+ subject { client.enabled? }
+
+ context 'when terms of service are accepted' do
+ it { is_expected.to eq(true) }
+
+ context "when private_key isn't present and database is read only" do
+ before do
+ allow(::Gitlab::Database).to receive(:read_only?).and_return(true)
+ end
+
+ it 'returns false' do
+ expect(::Gitlab::CurrentSettings.lets_encrypt_private_key).to eq(nil)
+
+ is_expected.to eq(false)
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when terms of service are not accepted' do
+ before do
+ stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#terms_of_service_url' do
+ subject { client.terms_of_service_url }
+
+ it 'returns valid url' do
+ is_expected.to eq("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lets_encrypt/order_spec.rb b/spec/lib/gitlab/lets_encrypt/order_spec.rb
new file mode 100644
index 00000000000..1a759103c44
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/order_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Order do
+ include LetsEncryptHelpers
+
+ let(:acme_order) { acme_order_double }
+
+ let(:order) { described_class.new(acme_order) }
+
+ LetsEncryptHelpers::ACME_ORDER_METHODS.each do |method, value|
+ describe "##{method}" do
+ it 'delegates to Acme::Client::Resources::Order' do
+ expect(order.public_send(method)).to eq(value)
+ end
+ end
+ end
+
+ describe '#new_challenge' do
+ it 'returns challenge' do
+ expect(order.new_challenge).to be_a(::Gitlab::LetsEncrypt::Challenge)
+ end
+ end
+
+ describe '#request_certificate' do
+ let(:private_key) do
+ OpenSSL::PKey::RSA.new(4096).to_pem
+ end
+
+ it 'generates csr and finalizes order' do
+ expect(acme_order).to receive(:finalize) do |csr:|
+ expect do
+ csr.csr # it's being evaluated lazily
+ end.not_to raise_error
+ end
+
+ order.request_certificate(domain: 'example.com', private_key: private_key)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index 8961ecc4be0..701ed1f3a1b 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -77,96 +77,42 @@ describe Gitlab::LfsToken, :clean_gitlab_redis_shared_state do
let(:actor) { create(:user, username: 'test_user_lfs_1') }
let(:lfs_token) { described_class.new(actor) }
- context 'for an HMAC token' do
- before do
- # We're not interested in testing LegacyRedisDeviseToken here
- allow(Gitlab::LfsToken::LegacyRedisDeviseToken).to receive_message_chain(:new, :token_valid?).and_return(false)
- end
-
- context 'where the token is invalid' do
- context "because it's junk" do
- it 'returns false' do
- expect(lfs_token.token_valid?('junk')).to be_falsey
- end
- end
-
- context "because it's been fiddled with" do
- it 'returns false' do
- fiddled_token = lfs_token.token.tap { |token| token[0] = 'E' }
- expect(lfs_token.token_valid?(fiddled_token)).to be_falsey
- end
- end
-
- context "because it was generated with a different secret" do
- it 'returns false' do
- different_actor = create(:user, username: 'test_user_lfs_2')
- different_secret_token = described_class.new(different_actor).token
- expect(lfs_token.token_valid?(different_secret_token)).to be_falsey
- end
- end
-
- context "because it's expired" do
- it 'returns false' do
- expired_token = lfs_token.token
- # Needs to be at least 1860 seconds, because the default expiry is
- # 1800 seconds with an additional 60 second leeway.
- Timecop.freeze(Time.now + 1865) do
- expect(lfs_token.token_valid?(expired_token)).to be_falsey
- end
- end
+ context 'where the token is invalid' do
+ context "because it's junk" do
+ it 'returns false' do
+ expect(lfs_token.token_valid?('junk')).to be_falsey
end
end
- context 'where the token is valid' do
- it 'returns true' do
- expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
+ context "because it's been fiddled with" do
+ it 'returns false' do
+ fiddled_token = lfs_token.token.tap { |token| token[0] = 'E' }
+ expect(lfs_token.token_valid?(fiddled_token)).to be_falsey
end
end
- end
-
- context 'for a LegacyRedisDevise token' do
- before do
- # We're not interested in testing HMACToken here
- allow_any_instance_of(Gitlab::LfsToken::HMACToken).to receive(:token_valid?).and_return(false)
- end
-
- context 'where the token is invalid' do
- context "because it's junk" do
- it 'returns false' do
- expect(lfs_token.token_valid?('junk')).to be_falsey
- end
- end
- context "because it's been fiddled with" do
- it 'returns false' do
- generated_token = Gitlab::LfsToken::LegacyRedisDeviseToken.new(actor).store_new_token
- fiddled_token = generated_token.tap { |token| token[0] = 'E' }
- expect(lfs_token.token_valid?(fiddled_token)).to be_falsey
- end
- end
-
- context "because it was generated with a different secret" do
- it 'returns false' do
- different_actor = create(:user, username: 'test_user_lfs_2')
- different_secret_token = described_class.new(different_actor).token
- expect(lfs_token.token_valid?(different_secret_token)).to be_falsey
- end
+ context "because it was generated with a different secret" do
+ it 'returns false' do
+ different_actor = create(:user, username: 'test_user_lfs_2')
+ different_secret_token = described_class.new(different_actor).token
+ expect(lfs_token.token_valid?(different_secret_token)).to be_falsey
end
+ end
- context "because it's expired" do
- it 'returns false' do
- generated_token = Gitlab::LfsToken::LegacyRedisDeviseToken.new(actor).store_new_token(1)
- # We need a real sleep here because we need to wait for redis to expire the key.
- sleep(0.01)
- expect(lfs_token.token_valid?(generated_token)).to be_falsey
+ context "because it's expired" do
+ it 'returns false' do
+ expired_token = lfs_token.token
+ # Needs to be at least 1860 seconds, because the default expiry is
+ # 1800 seconds with an additional 60 second leeway.
+ Timecop.freeze(Time.now + 1865) do
+ expect(lfs_token.token_valid?(expired_token)).to be_falsey
end
end
end
context 'where the token is valid' do
it 'returns true' do
- generated_token = Gitlab::LfsToken::LegacyRedisDeviseToken.new(actor).store_new_token
- expect(lfs_token.token_valid?(generated_token)).to be_truthy
+ expect(lfs_token.token_valid?(lfs_token.token)).to be_truthy
end
end
end
diff --git a/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
new file mode 100644
index 00000000000..18052b1991c
--- /dev/null
+++ b/spec/lib/gitlab/markdown_cache/active_record/extension_spec.rb
@@ -0,0 +1,179 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::MarkdownCache::ActiveRecord::Extension do
+ let(:klass) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'issues'
+ include CacheMarkdownField
+ cache_markdown_field :title, whitelisted: true
+ cache_markdown_field :description, pipeline: :single_line
+
+ attr_accessor :author, :project
+ end
+ end
+
+ let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+
+ let(:markdown) { '`Foo`' }
+ let(:html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Foo</code></p>' }
+
+ let(:updated_markdown) { '`Bar`' }
+ let(:updated_html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Bar</code></p>' }
+
+ context 'an unchanged markdown field' do
+ let(:thing) { klass.new(title: markdown) }
+
+ before do
+ thing.title = thing.title
+ thing.save
+ end
+
+ it { expect(thing.title).to eq(markdown) }
+ it { expect(thing.title_html).to eq(html) }
+ it { expect(thing.title_html_changed?).not_to be_truthy }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
+ end
+
+ context 'a changed markdown field' do
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+
+ before do
+ thing.title = updated_markdown
+ thing.save
+ end
+
+ it { expect(thing.title_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
+ end
+
+ context 'when a markdown field is set repeatedly to an empty string' do
+ it do
+ expect(thing).to receive(:refresh_markdown_cache).once
+ thing.title = ''
+ thing.save
+ thing.title = ''
+ thing.save
+ end
+ end
+
+ context 'when a markdown field is set repeatedly to a string which renders as empty html' do
+ it do
+ expect(thing).to receive(:refresh_markdown_cache).once
+ thing.title = '[//]: # (This is also a comment.)'
+ thing.save
+ thing.title = '[//]: # (This is also a comment.)'
+ thing.save
+ end
+ end
+
+ context 'a non-markdown field changed' do
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+
+ before do
+ thing.state = 'closed'
+ thing.save
+ end
+
+ it { expect(thing.state).to eq('closed') }
+ it { expect(thing.title).to eq(markdown) }
+ it { expect(thing.title_html).to eq(html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
+ end
+
+ context 'version is out of date' do
+ let(:thing) { klass.new(title: updated_markdown, title_html: html, cached_markdown_version: nil) }
+
+ before do
+ thing.save
+ end
+
+ it { expect(thing.title_html).to eq(updated_html) }
+ it { expect(thing.cached_markdown_version).to eq(cache_version) }
+ end
+
+ context 'when an invalidating field is changed' do
+ it 'invalidates the cache when project changes' do
+ thing.project = :new_project
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+ thing.save
+
+ expect(thing.title_html).to eq(updated_html)
+ expect(thing.description_html).to eq(updated_html)
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
+
+ it 'invalidates the cache when author changes' do
+ thing.author = :new_author
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+ thing.save
+
+ expect(thing.title_html).to eq(updated_html)
+ expect(thing.description_html).to eq(updated_html)
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
+ end
+
+ describe '.attributes' do
+ it 'excludes cache attributes that is blacklisted by default' do
+ expect(thing.attributes.keys.sort).not_to include(%w[description_html])
+ end
+ end
+
+ describe '#cached_html_up_to_date?' do
+ let(:thing) { klass.create(title: updated_markdown, title_html: html, cached_markdown_version: nil) }
+ subject { thing.cached_html_up_to_date?(:title) }
+
+ it 'returns false if markdown has been changed but html has not' do
+ thing.title = "changed!"
+
+ is_expected.to be_falsy
+ end
+
+ it 'returns true if markdown has not been changed but html has' do
+ thing.title_html = updated_html
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns true if markdown and html have both been changed' do
+ thing.title = updated_markdown
+ thing.title_html = updated_html
+
+ is_expected.to be_truthy
+ end
+
+ it 'returns false if the markdown field is set but the html is not' do
+ thing.title_html = nil
+
+ is_expected.to be_falsy
+ end
+ end
+
+ describe '#refresh_markdown_cache!' do
+ before do
+ thing.title = updated_markdown
+ end
+
+ it 'skips saving if not persisted' do
+ expect(thing).to receive(:persisted?).and_return(false)
+ expect(thing).not_to receive(:update_columns)
+
+ thing.refresh_markdown_cache!
+ end
+
+ it 'saves the changes' do
+ expect(thing).to receive(:persisted?).and_return(true)
+
+ expect(thing).to receive(:update_columns)
+ .with("title_html" => updated_html,
+ "description_html" => "",
+ "cached_markdown_version" => cache_version)
+
+ thing.refresh_markdown_cache!
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown_cache/field_data_spec.rb b/spec/lib/gitlab/markdown_cache/field_data_spec.rb
new file mode 100644
index 00000000000..393bf85aa43
--- /dev/null
+++ b/spec/lib/gitlab/markdown_cache/field_data_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+describe Gitlab::MarkdownCache::FieldData do
+ subject(:field_data) { described_class.new }
+
+ before do
+ field_data[:description] = { project: double('project in context') }
+ end
+
+ it 'translates a markdown field name into a html field name' do
+ expect(field_data.html_field(:description)).to eq("description_html")
+ end
+end
diff --git a/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
new file mode 100644
index 00000000000..b6a781de426
--- /dev/null
+++ b/spec/lib/gitlab/markdown_cache/redis/extension_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::MarkdownCache::Redis::Extension, :clean_gitlab_redis_cache do
+ let(:klass) do
+ Class.new do
+ include CacheMarkdownField
+
+ def initialize(title: nil, description: nil)
+ @title, @description = title, description
+ end
+
+ attr_reader :title, :description
+
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
+ def cache_key
+ "cache-key"
+ end
+ end
+ end
+
+ let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
+ let(:thing) { klass.new(title: "`Hello`", description: "`World`") }
+ let(:expected_cache_key) { "markdown_cache:cache-key" }
+
+ it 'defines the html attributes' do
+ expect(thing).to respond_to(:title_html, :description_html, :cached_markdown_version)
+ end
+
+ it 'loads the markdown from the cache only once' do
+ expect(thing).to receive(:load_cached_markdown).once.and_call_original
+
+ thing.title_html
+ thing.description_html
+ end
+
+ it 'correctly loads the markdown if it was stored in redis' do
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmset(expected_cache_key,
+ title_html: 'hello',
+ description_html: 'world',
+ cached_markdown_version: cache_version)
+ end
+
+ expect(thing.title_html).to eq('hello')
+ expect(thing.description_html).to eq('world')
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
+
+ describe "#refresh_markdown_cache!" do
+ it "stores the value in redis" do
+ expected_results = { "title_html" => "`Hello`",
+ "description_html" => "<p data-sourcepos=\"1:1-1:7\" dir=\"auto\"><code>World</code></p>",
+ "cached_markdown_version" => cache_version.to_s }
+
+ thing.refresh_markdown_cache!
+
+ results = Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmget(expected_cache_key,
+ "title_html", "description_html", "cached_markdown_version")
+ end
+
+ expect(results).to eq(expected_results)
+ end
+
+ it "assigns the values" do
+ thing.refresh_markdown_cache!
+
+ expect(thing.title_html).to eq('`Hello`')
+ expect(thing.description_html).to eq("<p data-sourcepos=\"1:1-1:7\" dir=\"auto\"><code>World</code></p>")
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown_cache/redis/store_spec.rb b/spec/lib/gitlab/markdown_cache/redis/store_spec.rb
new file mode 100644
index 00000000000..95c68e7d491
--- /dev/null
+++ b/spec/lib/gitlab/markdown_cache/redis/store_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::MarkdownCache::Redis::Store, :clean_gitlab_redis_cache do
+ let(:storable_class) do
+ Class.new do
+ cattr_reader :cached_markdown_fields do
+ Gitlab::MarkdownCache::FieldData.new.tap do |field_data|
+ field_data[:field_1] = {}
+ field_data[:field_2] = {}
+ end
+ end
+
+ attr_accessor :field_1, :field_2, :field_1_html, :field_2_html, :cached_markdown_version
+
+ def cache_key
+ "cache-key"
+ end
+ end
+ end
+ let(:storable) { storable_class.new }
+ let(:cache_key) { "markdown_cache:#{storable.cache_key}" }
+
+ subject(:store) { described_class.new(storable) }
+
+ def read_values
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmget(cache_key,
+ :field_1_html, :field_2_html, :cached_markdown_version)
+ end
+ end
+
+ def store_values(values)
+ Gitlab::Redis::Cache.with do |r|
+ r.mapped_hmset(cache_key,
+ values)
+ end
+ end
+
+ describe '#save' do
+ it 'stores updates to html fields and version' do
+ values_to_store = { field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1 }
+
+ store.save(values_to_store)
+
+ expect(read_values)
+ .to eq({ field_1_html: "hello", field_2_html: "world", cached_markdown_version: "1" })
+ end
+ end
+
+ describe '#read' do
+ it 'reads the html fields and version from redis if they were stored' do
+ stored_values = { field_1_html: "hello", field_2_html: "world", cached_markdown_version: 1 }
+
+ store_values(stored_values)
+
+ expect(store.read.symbolize_keys).to eq(stored_values)
+ end
+
+ it 'is mared loaded after reading' do
+ expect(store).not_to be_loaded
+
+ store.read
+
+ expect(store).to be_loaded
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/finder_spec.rb b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
new file mode 100644
index 00000000000..e88eb140b35
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/finder_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+ let(:system_dashboard_path) { Gitlab::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH}
+
+ describe '.find' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:service_call) { described_class.find(project, nil, environment, dashboard_path) }
+
+ it_behaves_like 'misconfigured dashboard service response', :not_found
+
+ context 'when the dashboard exists' do
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the system dashboard is specified' do
+ let(:dashboard_path) { system_dashboard_path }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+
+ context 'when no dashboard is specified' do
+ let(:service_call) { described_class.find(project, nil, environment) }
+
+ it_behaves_like 'valid dashboard service response'
+ end
+ end
+
+ describe '.find_all_paths' do
+ let(:all_dashboard_paths) { described_class.find_all_paths(project) }
+ let(:system_dashboard) { { path: system_dashboard_path, default: true } }
+
+ it 'includes only the system dashboard by default' do
+ expect(all_dashboard_paths).to eq([system_dashboard])
+ end
+
+ context 'when the project contains dashboards' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it 'includes system and project dashboards' do
+ project_dashboard = { path: dashboard_path, default: false }
+
+ expect(all_dashboard_paths).to contain_exactly(system_dashboard, project_dashboard)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
new file mode 100644
index 00000000000..be3c1095bd7
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Processor do
+ let(:project) { build(:project) }
+ let(:environment) { build(:environment, project: project) }
+ let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
+
+ describe 'process' do
+ let(:process_params) { [project, environment, dashboard_yml] }
+ let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: true) }
+
+ context 'when dashboard config corresponds to common metrics' do
+ let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
+
+ it 'inserts metric ids into the config' do
+ target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' }
+
+ expect(target_metric).to include(:metric_id)
+ expect(target_metric[:metric_id]).to eq(common_metric.id)
+ end
+ end
+
+ context 'when the project has associated metrics' do
+ let!(:project_response_metric) { create(:prometheus_metric, project: project, group: :response) }
+ let!(:project_system_metric) { create(:prometheus_metric, project: project, group: :system) }
+ let!(:project_business_metric) { create(:prometheus_metric, project: project, group: :business) }
+
+ it 'includes project-specific metrics' do
+ expect(all_metrics).to include get_metric_details(project_system_metric)
+ expect(all_metrics).to include get_metric_details(project_response_metric)
+ expect(all_metrics).to include get_metric_details(project_business_metric)
+ end
+
+ it 'orders groups by priority and panels by weight' do
+ expected_metrics_order = [
+ 'metric_b', # group priority 10, panel weight 1
+ 'metric_a2', # group priority 1, panel weight 2
+ 'metric_a1', # group priority 1, panel weight 1
+ project_business_metric.id, # group priority 0, panel weight nil (0)
+ project_response_metric.id, # group priority -5, panel weight nil (0)
+ project_system_metric.id, # group priority -10, panel weight nil (0)
+ ]
+ actual_metrics_order = all_metrics.map { |m| m[:id] || m[:metric_id] }
+
+ expect(actual_metrics_order).to eq expected_metrics_order
+ end
+
+ context 'when the dashboard should not include project metrics' do
+ let(:dashboard) { described_class.new(*process_params).process(insert_project_metrics: false) }
+
+ it 'includes only dashboard metrics' do
+ metrics = all_metrics.map { |m| m[:id] }
+
+ expect(metrics.length).to be(3)
+ expect(metrics).to eq %w(metric_b metric_a2 metric_a1)
+ end
+ end
+ end
+
+ shared_examples_for 'errors with message' do |expected_message|
+ it 'raises a DashboardLayoutError' do
+ error_class = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardLayoutError
+
+ expect { dashboard }.to raise_error(error_class, expected_message)
+ end
+ end
+
+ context 'when the dashboard is missing panel_groups' do
+ let(:dashboard_yml) { {} }
+
+ it_behaves_like 'errors with message', 'Top-level key :panel_groups must be an array'
+ end
+
+ context 'when the dashboard contains a panel_group which is missing panels' do
+ let(:dashboard_yml) { { panel_groups: [{}] } }
+
+ it_behaves_like 'errors with message', 'Each "panel_group" must define an array :panels'
+ end
+
+ context 'when the dashboard contains a panel which is missing metrics' do
+ let(:dashboard_yml) { { panel_groups: [{ panels: [{}] }] } }
+
+ it_behaves_like 'errors with message', 'Each "panel" must define an array :metrics'
+ end
+ end
+
+ private
+
+ def all_metrics
+ dashboard[:panel_groups].map do |group|
+ group[:panels].map { |panel| panel[:metrics] }
+ end.flatten
+ end
+
+ def get_metric_details(metric)
+ {
+ query_range: metric.query,
+ unit: metric.unit,
+ label: metric.legend,
+ metric_id: metric.id
+ }
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
new file mode 100644
index 00000000000..162beb0268a
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/project_dashboard_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::Metrics::Dashboard::ProjectDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:user) { build(:user) }
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe 'get_dashboard' do
+ let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
+ let(:service_params) { [project, user, { environment: environment, dashboard_path: dashboard_path }] }
+ let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+ context 'when the dashboard does not exist' do
+ it_behaves_like 'misconfigured dashboard service response', :not_found
+ end
+
+ context 'when the dashboard exists' do
+ let(:project) { project_with_dashboard(dashboard_path) }
+
+ it_behaves_like 'valid dashboard service response'
+
+ it 'caches the unprocessed dashboard for subsequent calls' do
+ expect_any_instance_of(described_class)
+ .to receive(:get_raw_dashboard)
+ .once
+ .and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+
+ context 'and the dashboard is then deleted' do
+ it 'does not return the previously cached dashboard' do
+ described_class.new(*service_params).get_dashboard
+
+ delete_project_dashboard(project, user, dashboard_path)
+
+ expect_any_instance_of(described_class)
+ .to receive(:get_raw_dashboard)
+ .once
+ .and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ end
+ end
+ end
+
+ context 'when the dashboard is configured incorrectly' do
+ let(:project) { project_with_dashboard(dashboard_path, {}) }
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
new file mode 100644
index 00000000000..e71ce2481a3
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/system_dashboard_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::SystemDashboardService, :use_clean_rails_memory_store_caching do
+ include MetricsDashboardHelpers
+
+ set(:project) { build(:project) }
+ set(:environment) { build(:environment, project: project) }
+
+ describe 'get_dashboard' do
+ let(:dashboard_path) { described_class::SYSTEM_DASHBOARD_PATH }
+ let(:service_params) { [project, nil, { environment: environment, dashboard_path: dashboard_path }] }
+ let(:service_call) { described_class.new(*service_params).get_dashboard }
+
+ it_behaves_like 'valid dashboard service response'
+
+ it 'caches the unprocessed dashboard for subsequent calls' do
+ expect(YAML).to receive(:safe_load).once.and_call_original
+
+ described_class.new(*service_params).get_dashboard
+ described_class.new(*service_params).get_dashboard
+ end
+
+ context 'when called with a non-system dashboard' do
+ let(:dashboard_path) { 'garbage/dashboard/path' }
+
+ # We want to alwaus return the system dashboard.
+ it_behaves_like 'valid dashboard service response'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
new file mode 100644
index 00000000000..f4a6e1fc7d9
--- /dev/null
+++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Samplers::PumaSampler do
+ subject { described_class.new(5) }
+ let(:null_metric) { double('null_metric', set: nil, observe: nil) }
+
+ before do
+ allow(Gitlab::Metrics::NullMetric).to receive(:instance).and_return(null_metric)
+ end
+
+ describe '#sample' do
+ before do
+ expect(subject).to receive(:puma_stats).and_return(puma_stats)
+ end
+
+ context 'in cluster mode' do
+ let(:puma_stats) do
+ <<~EOS
+ {
+ "workers": 2,
+ "phase": 2,
+ "booted_workers": 2,
+ "old_workers": 0,
+ "worker_status": [{
+ "pid": 32534,
+ "index": 0,
+ "phase": 1,
+ "booted": true,
+ "last_checkin": "2019-05-15T07:57:55Z",
+ "last_status": {
+ "backlog":0,
+ "running":1,
+ "pool_capacity":4,
+ "max_threads": 4
+ }
+ }]
+ }
+ EOS
+ end
+
+ it 'samples master statistics' do
+ labels = { worker: 'master' }
+
+ expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 2)
+ expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 2)
+ expect(subject.metrics[:puma_stale_workers]).to receive(:set).with(labels, 0)
+ expect(subject.metrics[:puma_phase]).to receive(:set).once.with(labels, 2)
+ expect(subject.metrics[:puma_phase]).to receive(:set).once.with({ worker: 'worker_0' }, 1)
+
+ subject.sample
+ end
+
+ it 'samples worker statistics' do
+ labels = { worker: 'worker_0' }
+
+ expect_worker_stats(labels)
+
+ subject.sample
+ end
+ end
+
+ context 'with empty worker stats' do
+ let(:puma_stats) do
+ <<~EOS
+ {
+ "workers": 2,
+ "phase": 2,
+ "booted_workers": 2,
+ "old_workers": 0,
+ "worker_status": [{
+ "pid": 32534,
+ "index": 0,
+ "phase": 1,
+ "booted": true,
+ "last_checkin": "2019-05-15T07:57:55Z",
+ "last_status": {}
+ }]
+ }
+ EOS
+ end
+
+ it 'does not log worker stats' do
+ expect(subject).not_to receive(:set_worker_metrics)
+
+ subject.sample
+ end
+ end
+
+ context 'in single mode' do
+ let(:puma_stats) do
+ <<~EOS
+ {
+ "backlog":0,
+ "running":1,
+ "pool_capacity":4,
+ "max_threads": 4
+ }
+ EOS
+ end
+
+ it 'samples worker statistics' do
+ labels = {}
+
+ expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 1)
+ expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 1)
+ expect_worker_stats(labels)
+
+ subject.sample
+ end
+ end
+ end
+
+ def expect_worker_stats(labels)
+ expect(subject.metrics[:puma_queued_connections]).to receive(:set).with(labels, 0)
+ expect(subject.metrics[:puma_active_connections]).to receive(:set).with(labels, 0)
+ expect(subject.metrics[:puma_running]).to receive(:set).with(labels, 1)
+ expect(subject.metrics[:puma_pool_capacity]).to receive(:set).with(labels, 4)
+ expect(subject.metrics[:puma_max_threads]).to receive(:set).with(labels, 4)
+ expect(subject.metrics[:puma_idle_threads]).to receive(:set).with(labels, 1)
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 7972ff253fe..aaf8c9fa2a0 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -10,17 +10,20 @@ describe Gitlab::Metrics::Samplers::RubySampler do
describe '#sample' do
it 'samples various statistics' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:cpu_time)
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:process_start_time)
+ expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors)
expect(sampler).to receive(:sample_gc)
sampler.sample
end
- it 'adds a metric containing the memory usage' do
+ it 'adds a metric containing the process resident memory bytes' do
expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000)
- expect(sampler.metrics[:memory_usage]).to receive(:set).with({}, 9000)
+ expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
sampler.sample
end
@@ -34,6 +37,27 @@ describe Gitlab::Metrics::Samplers::RubySampler do
sampler.sample
end
+ it 'adds a metric containing the process total cpu time' do
+ expect(Gitlab::Metrics::System).to receive(:cpu_time).and_return(0.51)
+ expect(sampler.metrics[:process_cpu_seconds_total]).to receive(:set).with({}, 0.51)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process start time' do
+ expect(Gitlab::Metrics::System).to receive(:process_start_time).and_return(12345)
+ expect(sampler.metrics[:process_start_time_seconds]).to receive(:set).with({}, 12345)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process max file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors).and_return(1024)
+ expect(sampler.metrics[:process_max_fds]).to receive(:set).with({}, 1024)
+
+ sampler.sample
+ end
+
it 'clears any GC profiles' do
expect(GC::Profiler).to receive(:clear)
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
index 4b03f3c2532..090e456644f 100644
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -39,8 +39,8 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'unix', socket_address: socket_address }
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+ expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
+ expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
subject.sample
end
@@ -50,7 +50,6 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
context 'unicorn listens on tcp sockets' do
let(:tcp_socket_address) { '0.0.0.0:8080' }
let(:tcp_sockets) { [tcp_socket_address] }
-
before do
allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
end
@@ -71,13 +70,29 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'tcp', socket_address: tcp_socket_address }
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+ expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
+ expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
subject.sample
end
end
end
+
+ context 'additional metrics' do
+ let(:unicorn_workers) { 2 }
+
+ before do
+ allow(unicorn).to receive(:listener_names).and_return([""])
+ allow(::Gitlab::Metrics::System).to receive(:cpu_time).and_return(3.14)
+ allow(subject).to receive(:unicorn_workers_count).and_return(unicorn_workers)
+ end
+
+ it "sets additional metrics" do
+ expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, unicorn_workers)
+
+ subject.sample
+ end
+ end
end
describe '#start' do
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 14afcdf5daa..b0603d96eb2 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -13,6 +13,18 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to be > 0
end
end
+
+ describe '.max_open_file_descriptors' do
+ it 'returns the max allowed open file descriptors' do
+ expect(described_class.max_open_file_descriptors).to be > 0
+ end
+ end
+
+ describe '.process_start_time' do
+ it 'returns the process start time' do
+ expect(described_class.process_start_time).to be > 0
+ end
+ end
else
describe '.memory_usage' do
it 'returns 0.0' do
@@ -25,6 +37,18 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to eq(0)
end
end
+
+ describe '.max_open_file_descriptors' do
+ it 'returns 0' do
+ expect(described_class.max_open_file_descriptors).to eq(0)
+ end
+ end
+
+ describe 'process_start_time' do
+ it 'returns 0' do
+ expect(described_class.process_start_time).to eq(0)
+ end
+ end
end
describe '.cpu_time' do
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
new file mode 100644
index 00000000000..e70fde09edd
--- /dev/null
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -0,0 +1,229 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Transaction do
+ let(:transaction) { described_class.new }
+ let(:metric) { transaction.metrics[0] }
+
+ let(:sensitive_tags) do
+ {
+ path: 'private',
+ branch: 'sensitive'
+ }
+ end
+
+ shared_examples 'tag filter' do |sane_tags|
+ it 'filters potentially sensitive tags' do
+ expect(metric.tags).to eq(sane_tags)
+ end
+ end
+
+ describe '#duration' do
+ it 'returns the duration of a transaction in seconds' do
+ transaction.run { }
+
+ expect(transaction.duration).to be > 0
+ end
+ end
+
+ describe '#allocated_memory' do
+ it 'returns the allocated memory in bytes' do
+ transaction.run { 'a' * 32 }
+
+ expect(transaction.allocated_memory).to be_a_kind_of(Numeric)
+ end
+ end
+
+ describe '#run' do
+ it 'yields the supplied block' do
+ expect { |b| transaction.run(&b) }.to yield_control
+ end
+
+ it 'stores the transaction in the current thread' do
+ transaction.run do
+ expect(described_class.current).to eq(transaction)
+ end
+ end
+
+ it 'removes the transaction from the current thread upon completion' do
+ transaction.run { }
+
+ expect(described_class.current).to be_nil
+ end
+ end
+
+ describe '#add_metric' do
+ it 'adds a metric to the transaction' do
+ transaction.add_metric('foo', value: 1)
+
+ expect(metric.series).to eq('rails_foo')
+ expect(metric.tags).to eq({})
+ expect(metric.values).to eq(value: 1)
+ end
+
+ context 'with sensitive tags' do
+ before do
+ transaction
+ .add_metric('foo', { value: 1 }, **sensitive_tags.merge(sane: 'yes'))
+ end
+
+ it_behaves_like 'tag filter', sane: 'yes'
+ 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 '#increment' do
+ it 'increments a counter' do
+ transaction.increment(:time, 1)
+ transaction.increment(:time, 2)
+
+ values = metric_values(time: 3)
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#set' do
+ it 'sets a value' do
+ transaction.set(:number, 10)
+
+ values = metric_values(number: 10)
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#finish' do
+ it 'tracks the transaction details and submits them to Sidekiq' do
+ expect(transaction).to receive(:track_self)
+ expect(transaction).to receive(:submit)
+
+ transaction.finish
+ end
+ end
+
+ describe '#track_self' do
+ it 'adds a metric for the transaction itself' do
+ values = metric_values
+
+ expect(transaction).to receive(:add_metric)
+ .with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#submit' do
+ it 'submits the metrics to Sidekiq' do
+ transaction.track_self
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([an_instance_of(Hash)])
+
+ transaction.submit
+ end
+
+ it 'adds the action as a tag for every metric' do
+ allow(transaction)
+ .to receive(:labels)
+ .and_return(controller: 'Foo', action: 'bar')
+
+ transaction.track_self
+
+ hash = {
+ series: 'rails_transactions',
+ tags: { action: 'Foo#bar' },
+ values: metric_values,
+ timestamp: a_kind_of(Integer)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([hash])
+
+ transaction.submit
+ end
+
+ it 'does not add an action tag for events' do
+ allow(transaction)
+ .to receive(:labels)
+ .and_return(controller: 'Foo', action: 'bar')
+
+ transaction.add_event(:meow)
+
+ hash = {
+ series: 'events',
+ tags: { event: :meow },
+ values: { count: 1 },
+ timestamp: a_kind_of(Integer)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics)
+ .with([hash])
+
+ transaction.submit
+ end
+ end
+
+ describe '#add_event' do
+ it 'adds a metric' do
+ transaction.add_event(:meow)
+
+ expect(metric).to be_an_instance_of(Gitlab::Metrics::Metric)
+ end
+
+ it "does not prefix the metric's series name" do
+ transaction.add_event(:meow)
+
+ expect(metric.series).to eq(described_class::EVENT_SERIES)
+ end
+
+ it 'tracks a counter for every event' do
+ transaction.add_event(:meow)
+
+ expect(metric.values).to eq(count: 1)
+ end
+
+ it 'tracks the event name' do
+ transaction.add_event(:meow)
+
+ expect(metric.tags).to eq(event: :meow)
+ end
+
+ it 'allows tracking of custom tags' do
+ transaction.add_event(:meow, animal: 'cat')
+
+ expect(metric.tags).to eq(event: :meow, animal: 'cat')
+ end
+
+ context 'with sensitive tags' do
+ before do
+ transaction.add_event(:meow, **sensitive_tags.merge(sane: 'yes'))
+ end
+
+ it_behaves_like 'tag filter', event: :meow, sane: 'yes'
+ end
+ end
+
+ private
+
+ def metric_values(opts = {})
+ {
+ duration: 0.0,
+ allocated_memory: a_kind_of(Numeric)
+ }.merge(opts)
+ end
+end
diff --git a/spec/lib/gitlab/middleware/basic_health_check_spec.rb b/spec/lib/gitlab/middleware/basic_health_check_spec.rb
index 187d903a5e1..86bdc479b66 100644
--- a/spec/lib/gitlab/middleware/basic_health_check_spec.rb
+++ b/spec/lib/gitlab/middleware/basic_health_check_spec.rb
@@ -28,6 +28,35 @@ describe Gitlab::Middleware::BasicHealthCheck do
end
end
+ context 'with X-Forwarded-For headers' do
+ let(:load_balancer_ip) { '1.2.3.4' }
+
+ before do
+ env['HTTP_X_FORWARDED_FOR'] = "#{load_balancer_ip}, 127.0.0.1"
+ env['REMOTE_ADDR'] = '127.0.0.1'
+ env['PATH_INFO'] = described_class::HEALTH_PATH
+ end
+
+ it 'returns 200 response when endpoint is allowed' do
+ allow(Settings.monitoring).to receive(:ip_whitelist).and_return([load_balancer_ip])
+ expect(app).not_to receive(:call)
+
+ response = middleware.call(env)
+
+ expect(response[0]).to eq(200)
+ expect(response[1]).to eq({ 'Content-Type' => 'text/plain' })
+ expect(response[2]).to eq(['GitLab OK'])
+ end
+
+ it 'returns 404 when whitelist is not configured' do
+ allow(Settings.monitoring).to receive(:ip_whitelist).and_return([])
+
+ response = middleware.call(env)
+
+ expect(response[0]).to eq(404)
+ end
+ end
+
context 'whitelisted IP' do
before do
env['REMOTE_ADDR'] = '127.0.0.1'
diff --git a/spec/lib/gitlab/namespaced_session_store_spec.rb b/spec/lib/gitlab/namespaced_session_store_spec.rb
new file mode 100644
index 00000000000..c0af2ede32a
--- /dev/null
+++ b/spec/lib/gitlab/namespaced_session_store_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::NamespacedSessionStore do
+ let(:key) { :some_key }
+ subject { described_class.new(key) }
+
+ it 'stores data under the specified key' do
+ Gitlab::Session.with_session({}) do
+ subject[:new_data] = 123
+
+ expect(Thread.current[:session_storage][key]).to eq(new_data: 123)
+ end
+ end
+
+ it 'retrieves data from the given key' do
+ Thread.current[:session_storage] = { key => { existing_data: 123 } }
+
+ expect(subject[:existing_data]).to eq 123
+ end
+end
diff --git a/spec/lib/gitlab/object_hierarchy_spec.rb b/spec/lib/gitlab/object_hierarchy_spec.rb
index 4700a7ad2e1..e6e9ae3223e 100644
--- a/spec/lib/gitlab/object_hierarchy_spec.rb
+++ b/spec/lib/gitlab/object_hierarchy_spec.rb
@@ -81,6 +81,24 @@ describe Gitlab::ObjectHierarchy, :postgresql do
expect { relation.update_all(share_with_group_lock: false) }
.to raise_error(ActiveRecord::ReadOnlyRecord)
end
+
+ context 'when with_depth is true' do
+ let(:relation) do
+ described_class.new(Group.where(id: parent.id)).base_and_descendants(with_depth: true)
+ end
+
+ it 'includes depth in the results' do
+ object_depths = {
+ parent.id => 1,
+ child1.id => 2,
+ child2.id => 3
+ }
+
+ relation.each do |object|
+ expect(object.depth).to eq(object_depths[object.id])
+ end
+ end
+ end
end
describe '#descendants' do
@@ -91,6 +109,28 @@ describe Gitlab::ObjectHierarchy, :postgresql do
end
end
+ describe '#max_descendants_depth' do
+ subject { described_class.new(base_relation).max_descendants_depth }
+
+ context 'when base relation is empty' do
+ let(:base_relation) { Group.where(id: nil) }
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'when base has no children' do
+ let(:base_relation) { Group.where(id: child2) }
+
+ it { expect(subject).to eq(1) }
+ end
+
+ context 'when base has grandchildren' do
+ let(:base_relation) { Group.where(id: parent) }
+
+ it { expect(subject).to eq(3) }
+ end
+ end
+
describe '#ancestors' do
it 'includes only the ancestors' do
relation = described_class.new(Group.where(id: child2)).ancestors
diff --git a/spec/lib/gitlab/omniauth_initializer_spec.rb b/spec/lib/gitlab/omniauth_initializer_spec.rb
index d808b4d49e0..32296caf819 100644
--- a/spec/lib/gitlab/omniauth_initializer_spec.rb
+++ b/spec/lib/gitlab/omniauth_initializer_spec.rb
@@ -38,6 +38,28 @@ describe Gitlab::OmniauthInitializer do
subject.execute([hash_config])
end
+ it 'normalizes a String strategy_class' do
+ hash_config = { 'name' => 'hash', 'args' => { strategy_class: 'OmniAuth::Strategies::OAuth2Generic' } }
+
+ expect(devise_config).to receive(:omniauth).with(:hash, strategy_class: OmniAuth::Strategies::OAuth2Generic)
+
+ subject.execute([hash_config])
+ end
+
+ it 'allows a class to be specified in strategy_class' do
+ hash_config = { 'name' => 'hash', 'args' => { strategy_class: OmniAuth::Strategies::OAuth2Generic } }
+
+ expect(devise_config).to receive(:omniauth).with(:hash, strategy_class: OmniAuth::Strategies::OAuth2Generic)
+
+ subject.execute([hash_config])
+ end
+
+ it 'throws an error for an invalid strategy_class' do
+ hash_config = { 'name' => 'hash', 'args' => { strategy_class: 'OmniAuth::Strategies::Bogus' } }
+
+ expect { subject.execute([hash_config]) }.to raise_error(NameError)
+ end
+
it 'configures fail_with_empty_uid for shibboleth' do
shibboleth_config = { 'name' => 'shibboleth', 'args' => {} }
@@ -61,5 +83,13 @@ describe Gitlab::OmniauthInitializer do
subject.execute([cas3_config])
end
+
+ it 'configures name for openid_connect' do
+ openid_connect_config = { 'name' => 'openid_connect', 'args' => {} }
+
+ expect(devise_config).to receive(:omniauth).with(:openid_connect, name: 'openid_connect')
+
+ subject.execute([openid_connect_config])
+ end
end
end
diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb
index 81f81d4f963..6fdf61ee0a7 100644
--- a/spec/lib/gitlab/optimistic_locking_spec.rb
+++ b/spec/lib/gitlab/optimistic_locking_spec.rb
@@ -6,7 +6,7 @@ describe Gitlab::OptimisticLocking do
describe '#retry_lock' do
it 'does not reload object if state changes' do
- expect(pipeline).not_to receive(:reload)
+ expect(pipeline).not_to receive(:reset)
expect(pipeline).to receive(:succeed).and_call_original
described_class.retry_lock(pipeline) do |subject|
@@ -17,7 +17,7 @@ describe Gitlab::OptimisticLocking do
it 'retries action if exception is raised' do
pipeline.succeed
- expect(pipeline2).to receive(:reload).and_call_original
+ expect(pipeline2).to receive(:reset).and_call_original
expect(pipeline2).to receive(:drop).twice.and_call_original
described_class.retry_lock(pipeline2) do |subject|
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index e90e0aba0a4..84b2e2dc823 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -100,7 +100,7 @@ describe Gitlab::PathRegex do
end
let(:ee_top_level_words) do
- ['unsubscribes']
+ %w(unsubscribes v2)
end
let(:files_in_public) do
@@ -120,10 +120,10 @@ describe Gitlab::PathRegex do
# - Followed by one or more path-parts not starting with `:` or `*`
# - Followed by a path-part that includes a wildcard parameter `*`
# At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw
- STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
- NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
- ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
- WILDCARD_SEGMENT = /\*/
+ STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}.freeze
+ NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}.freeze
+ ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}.freeze
+ WILDCARD_SEGMENT = /\*/.freeze
let(:namespaced_wildcard_routes) do
routes_without_format.select do |p|
p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
@@ -144,7 +144,7 @@ describe Gitlab::PathRegex do
end.uniq
end
- STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
+ STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}.freeze
let(:group_routes) do
routes_without_format.select do |path|
path =~ STARTING_WITH_GROUP
diff --git a/spec/lib/gitlab/phabricator_import/base_worker_spec.rb b/spec/lib/gitlab/phabricator_import/base_worker_spec.rb
new file mode 100644
index 00000000000..d46d908a3e3
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/base_worker_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::BaseWorker do
+ let(:subclass) do
+ # Creating an anonymous class for a worker is complicated, as we generate the
+ # queue name from the class name.
+ Gitlab::PhabricatorImport::ImportTasksWorker
+ end
+
+ describe '.schedule' do
+ let(:arguments) { %w[project_id the_next_page] }
+
+ it 'schedules the job' do
+ expect(subclass).to receive(:perform_async).with(*arguments)
+
+ subclass.schedule(*arguments)
+ end
+
+ it 'counts the scheduled job', :clean_gitlab_redis_shared_state do
+ state = Gitlab::PhabricatorImport::WorkerState.new('project_id')
+
+ allow(subclass).to receive(:remove_job) # otherwise the job is removed before we saw it
+
+ expect { subclass.schedule(*arguments) }.to change { state.running_count }.by(1)
+ end
+ end
+
+ describe '#perform' do
+ let(:project) { create(:project, :import_started, import_url: "https://a.phab.instance") }
+ let(:worker) { subclass.new }
+ let(:state) { Gitlab::PhabricatorImport::WorkerState.new(project.id) }
+
+ before do
+ allow(worker).to receive(:import)
+ end
+
+ it 'does not break for a non-existing project' do
+ expect { worker.perform('not a thing') }.not_to raise_error
+ end
+
+ it 'does not do anything when the import is not in progress' do
+ project = create(:project, :import_failed)
+
+ expect(worker).not_to receive(:import)
+
+ worker.perform(project.id)
+ end
+
+ it 'calls import for the project' do
+ expect(worker).to receive(:import).with(project, 'other_arg')
+
+ worker.perform(project.id, 'other_arg')
+ end
+
+ it 'marks the project as imported if there was only one job running' do
+ worker.perform(project.id)
+
+ expect(project.import_state.reload).to be_finished
+ end
+
+ it 'does not mark the job as finished when there are more scheduled jobs' do
+ 2.times { state.add_job }
+
+ worker.perform(project.id)
+
+ expect(project.import_state.reload).to be_in_progress
+ end
+
+ it 'decrements the job counter' do
+ expect { worker.perform(project.id) }.to change { state.running_count }.by(-1)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/cache/map_spec.rb b/spec/lib/gitlab/phabricator_import/cache/map_spec.rb
new file mode 100644
index 00000000000..52c7a02219f
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/cache/map_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Cache::Map, :clean_gitlab_redis_cache do
+ set(:project) { create(:project) }
+ let(:redis) { Gitlab::Redis::Cache }
+ subject(:map) { described_class.new(project) }
+
+ describe '#get_gitlab_model' do
+ it 'returns nil if there was nothing cached for the phabricator id' do
+ expect(map.get_gitlab_model('does not exist')).to be_nil
+ end
+
+ it 'returns the object if it was set in redis' do
+ issue = create(:issue, project: project)
+ set_in_redis('exists', issue)
+
+ expect(map.get_gitlab_model('exists')).to eq(issue)
+ end
+
+ it 'extends the TTL for the cache key' do
+ set_in_redis('extend', create(:issue, project: project)) do |redis|
+ redis.expire(cache_key('extend'), 10.seconds.to_i)
+ end
+
+ map.get_gitlab_model('extend')
+
+ ttl = redis.with { |redis| redis.ttl(cache_key('extend')) }
+
+ expect(ttl).to be > 10.seconds
+ end
+ end
+
+ describe '#set_gitlab_model' do
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ it 'sets the class and id in redis with a ttl' do
+ issue = create(:issue, project: project)
+
+ map.set_gitlab_model(issue, 'it is set')
+
+ set_data, ttl = redis.with do |redis|
+ redis.pipelined do |p|
+ p.mapped_hmget(cache_key('it is set'), :classname, :database_id)
+ p.ttl(cache_key('it is set'))
+ end
+ end
+
+ expect(set_data).to eq({ classname: 'Issue', database_id: issue.id.to_s })
+ expect(ttl).to be_within(1.second).of(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ end
+ end
+
+ def set_in_redis(key, object)
+ redis.with do |redis|
+ redis.mapped_hmset(cache_key(key),
+ { classname: object.class, database_id: object.id })
+ yield(redis) if block_given?
+ end
+ end
+
+ def cache_key(phabricator_id)
+ subject.__send__(:cache_key_for_phabricator_id, phabricator_id)
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb
new file mode 100644
index 00000000000..542b3cd060f
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/conduit/client_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Conduit::Client do
+ let(:client) do
+ described_class.new('https://see-ya-later.phabricator', 'api-token')
+ end
+
+ describe '#get' do
+ it 'performs and parses a request' do
+ params = { some: 'extra', values: %w[are passed] }
+ stub_valid_request(params)
+
+ response = client.get('test', params: params)
+
+ expect(response).to be_a(Gitlab::PhabricatorImport::Conduit::Response)
+ expect(response).to be_success
+ end
+
+ it 'wraps request errors in an `ApiError`' do
+ stub_timeout
+
+ expect { client.get('test') }.to raise_error(Gitlab::PhabricatorImport::Conduit::ApiError)
+ end
+
+ it 'raises response error' do
+ stub_error_response
+
+ expect { client.get('test') }
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /has the wrong length/)
+ end
+ end
+
+ def stub_valid_request(params = {})
+ WebMock.stub_request(
+ :get, 'https://see-ya-later.phabricator/api/test'
+ ).with(
+ body: CGI.unescape(params.reverse_merge('api.token' => 'api-token').to_query)
+ ).and_return(
+ status: 200,
+ body: fixture_file('phabricator_responses/maniphest.search.json')
+ )
+ end
+
+ def stub_timeout
+ WebMock.stub_request(
+ :get, 'https://see-ya-later.phabricator/api/test'
+ ).to_timeout
+ end
+
+ def stub_error_response
+ WebMock.stub_request(
+ :get, 'https://see-ya-later.phabricator/api/test'
+ ).and_return(
+ status: 200,
+ body: fixture_file('phabricator_responses/auth_failed.json')
+ )
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb
new file mode 100644
index 00000000000..0d7714649b9
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/conduit/maniphest_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Conduit::Maniphest do
+ let(:maniphest) do
+ described_class.new(phabricator_url: 'https://see-ya-later.phabricator', api_token: 'api-token')
+ end
+
+ describe '#tasks' do
+ let(:fake_client) { double('Phabricator client') }
+
+ before do
+ allow(maniphest).to receive(:client).and_return(fake_client)
+ end
+
+ it 'calls the api with the correct params' do
+ expected_params = {
+ after: '123',
+ attachments: {
+ projects: 1, subscribers: 1, columns: 1
+ }
+ }
+
+ expect(fake_client).to receive(:get).with('maniphest.search',
+ params: expected_params)
+
+ maniphest.tasks(after: '123')
+ end
+
+ it 'returns a parsed response' do
+ response = Gitlab::PhabricatorImport::Conduit::Response
+ .new(fixture_file('phabricator_responses/maniphest.search.json'))
+
+ allow(fake_client).to receive(:get).and_return(response)
+
+ expect(maniphest.tasks).to be_a(Gitlab::PhabricatorImport::Conduit::TasksResponse)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
new file mode 100644
index 00000000000..a8596968f14
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/conduit/response_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Conduit::Response do
+ let(:response) { described_class.new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))}
+ let(:error_response) { described_class.new(JSON.parse(fixture_file('phabricator_responses/auth_failed.json'))) }
+
+ describe '.parse!' do
+ it 'raises a ResponseError if the http response was not successfull' do
+ fake_response = double(:http_response, success?: false, status: 401)
+
+ expect { described_class.parse!(fake_response) }
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /responded with 401/)
+ end
+
+ it 'raises a ResponseError if the response contained a Phabricator error' do
+ fake_response = double(:http_response,
+ success?: true,
+ status: 200,
+ body: fixture_file('phabricator_responses/auth_failed.json'))
+
+ expect { described_class.parse!(fake_response) }
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /ERR-INVALID-AUTH: API token/)
+ end
+
+ it 'raises a ResponseError if JSON parsing failed' do
+ fake_response = double(:http_response,
+ success?: true,
+ status: 200,
+ body: 'This is no JSON')
+
+ expect { described_class.parse!(fake_response) }
+ .to raise_error(Gitlab::PhabricatorImport::Conduit::ResponseError, /unexpected token at/)
+ end
+
+ it 'returns a parsed response for valid input' do
+ fake_response = double(:http_response,
+ success?: true,
+ status: 200,
+ body: fixture_file('phabricator_responses/maniphest.search.json'))
+
+ expect(described_class.parse!(fake_response)).to be_a(described_class)
+ end
+ end
+
+ describe '#success?' do
+ it { expect(response).to be_success }
+ it { expect(error_response).not_to be_success }
+ end
+
+ describe '#error_code' do
+ it { expect(error_response.error_code).to eq('ERR-INVALID-AUTH') }
+ it { expect(response.error_code).to be_nil }
+ end
+
+ describe '#error_info' do
+ it 'returns the correct error info' do
+ expected_message = 'API token "api-token" has the wrong length. API tokens should be 32 characters long.'
+
+ expect(error_response.error_info).to eq(expected_message)
+ end
+
+ it { expect(response.error_info).to be_nil }
+ end
+
+ describe '#data' do
+ it { expect(error_response.data).to be_nil }
+ it { expect(response.data).to be_an(Array) }
+ end
+
+ describe '#pagination' do
+ it { expect(error_response.pagination).to be_nil }
+
+ it 'builds the pagination correctly' do
+ expect(response.pagination).to be_a(Gitlab::PhabricatorImport::Conduit::Pagination)
+ expect(response.pagination.next_page).to eq('284')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb b/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb
new file mode 100644
index 00000000000..4b4c2a6276e
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/conduit/tasks_response_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Conduit::TasksResponse do
+ let(:conduit_response) do
+ Gitlab::PhabricatorImport::Conduit::Response
+ .new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))
+ end
+
+ subject(:response) { described_class.new(conduit_response) }
+
+ describe '#pagination' do
+ it 'delegates to the conduit reponse' do
+ expect(response.pagination).to eq(conduit_response.pagination)
+ end
+ end
+
+ describe '#tasks' do
+ it 'builds the correct tasks representation' do
+ tasks = response.tasks
+
+ titles = tasks.map(&:issue_attributes).map { |attrs| attrs[:title] }
+
+ expect(titles).to contain_exactly('Things are slow', 'Things are broken')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb b/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb
new file mode 100644
index 00000000000..1e38ef8aaa5
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/import_tasks_worker_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::ImportTasksWorker do
+ describe '#perform' do
+ it 'calls the correct importer' do
+ project = create(:project, :import_started, import_url: "https://the.phab.ulr")
+ fake_importer = instance_double(Gitlab::PhabricatorImport::Issues::Importer)
+
+ expect(Gitlab::PhabricatorImport::Issues::Importer).to receive(:new).with(project).and_return(fake_importer)
+ expect(fake_importer).to receive(:execute)
+
+ described_class.new.perform(project.id)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/importer_spec.rb b/spec/lib/gitlab/phabricator_import/importer_spec.rb
new file mode 100644
index 00000000000..bf14010a187
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/importer_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Importer do
+ it { expect(described_class).to be_async }
+
+ it "acts like it's importing repositories" do
+ expect(described_class).to be_imports_repository
+ end
+
+ describe '#execute' do
+ let(:project) { create(:project, :import_scheduled) }
+ subject(:importer) { described_class.new(project) }
+
+ it 'sets a custom jid that will be kept up to date' do
+ expect { importer.execute }.to change { project.import_state.reload.jid }
+ end
+
+ it 'starts importing tasks' do
+ expect(Gitlab::PhabricatorImport::ImportTasksWorker).to receive(:schedule).with(project.id)
+
+ importer.execute
+ end
+
+ it 'marks the import as failed when something goes wrong' do
+ allow(importer).to receive(:schedule_first_tasks_page).and_raise('Stuff is broken')
+
+ importer.execute
+
+ expect(project.import_state).to be_failed
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb b/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb
new file mode 100644
index 00000000000..2412cf76f79
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/issues/importer_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Issues::Importer do
+ set(:project) { create(:project) }
+
+ let(:response) do
+ Gitlab::PhabricatorImport::Conduit::TasksResponse.new(
+ Gitlab::PhabricatorImport::Conduit::Response
+ .new(JSON.parse(fixture_file('phabricator_responses/maniphest.search.json')))
+ )
+ end
+
+ subject(:importer) { described_class.new(project, nil) }
+
+ before do
+ client = instance_double(Gitlab::PhabricatorImport::Conduit::Maniphest)
+
+ allow(client).to receive(:tasks).and_return(response)
+ allow(importer).to receive(:client).and_return(client)
+ end
+
+ describe '#execute' do
+ it 'imports each task in the response' do
+ response.tasks.each do |task|
+ task_importer = instance_double(Gitlab::PhabricatorImport::Issues::TaskImporter)
+
+ expect(task_importer).to receive(:execute)
+ expect(Gitlab::PhabricatorImport::Issues::TaskImporter)
+ .to receive(:new).with(project, task)
+ .and_return(task_importer)
+ end
+
+ importer.execute
+ end
+
+ it 'schedules the next batch if there is one' do
+ expect(Gitlab::PhabricatorImport::ImportTasksWorker)
+ .to receive(:schedule).with(project.id, response.pagination.next_page)
+
+ importer.execute
+ end
+
+ it 'does not reschedule when there is no next page' do
+ allow(response.pagination).to receive(:has_next_page?).and_return(false)
+
+ expect(Gitlab::PhabricatorImport::ImportTasksWorker)
+ .not_to receive(:schedule)
+
+ importer.execute
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb b/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb
new file mode 100644
index 00000000000..1625604e754
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/issues/task_importer_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Issues::TaskImporter do
+ set(:project) { create(:project) }
+ let(:task) do
+ Gitlab::PhabricatorImport::Representation::Task.new(
+ {
+ 'phid' => 'the-phid',
+ 'fields' => {
+ 'name' => 'Title',
+ 'description' => {
+ 'raw' => '# This is markdown\n it can contain more text.'
+ },
+ 'dateCreated' => '1518688921',
+ 'dateClosed' => '1518789995'
+ }
+ }
+ )
+ end
+
+ describe '#execute' do
+ it 'creates the issue with the expected attributes' do
+ issue = described_class.new(project, task).execute
+
+ expect(issue.project).to eq(project)
+ expect(issue).to be_persisted
+ expect(issue.author).to eq(User.ghost)
+ expect(issue.title).to eq('Title')
+ expect(issue.description).to eq('# This is markdown\n it can contain more text.')
+ expect(issue).to be_closed
+ expect(issue.created_at).to eq(Time.at(1518688921))
+ expect(issue.closed_at).to eq(Time.at(1518789995))
+ end
+
+ it 'does not recreate the issue when called multiple times' do
+ expect { described_class.new(project, task).execute }
+ .to change { project.issues.reload.size }.from(0).to(1)
+ expect { described_class.new(project, task).execute }
+ .not_to change { project.issues.reload.size }
+ end
+
+ it 'does not trigger a save when the object did not change' do
+ existing_issue = create(:issue,
+ task.issue_attributes.merge(author: User.ghost))
+ importer = described_class.new(project, task)
+ allow(importer).to receive(:issue).and_return(existing_issue)
+
+ expect(existing_issue).not_to receive(:save!)
+
+ importer.execute
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/project_creator_spec.rb b/spec/lib/gitlab/phabricator_import/project_creator_spec.rb
new file mode 100644
index 00000000000..e9455b866ac
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/project_creator_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::ProjectCreator do
+ let(:user) { create(:user) }
+ let(:params) do
+ { path: 'new-phab-import',
+ phabricator_server_url: 'http://phab.example.com',
+ api_token: 'the-token' }
+ end
+ subject(:creator) { described_class.new(user, params) }
+
+ describe '#execute' do
+ it 'creates a project correctly and schedule an import' do
+ expect_next_instance_of(Gitlab::PhabricatorImport::Importer) do |importer|
+ expect(importer).to receive(:execute)
+ end
+
+ project = creator.execute
+
+ expect(project).to be_persisted
+ expect(project).to be_import
+ expect(project.import_type).to eq('phabricator')
+ expect(project.import_data.credentials).to match(a_hash_including(api_token: 'the-token'))
+ expect(project.import_data.data).to match(a_hash_including('phabricator_url' => 'http://phab.example.com'))
+ expect(project.import_url).to eq(Project::UNKNOWN_IMPORT_URL)
+ expect(project.namespace).to eq(user.namespace)
+ end
+
+ context 'when import params are missing' do
+ let(:params) do
+ { path: 'new-phab-import',
+ phabricator_server_url: 'http://phab.example.com',
+ api_token: '' }
+ end
+
+ it 'returns nil' do
+ expect(creator.execute).to be_nil
+ end
+ end
+
+ context 'when import params are invalid' do
+ let(:params) do
+ { path: 'new-phab-import',
+ namespace_id: '-1',
+ phabricator_server_url: 'http://phab.example.com',
+ api_token: 'the-token' }
+ end
+
+ it 'returns an unpersisted project' do
+ project = creator.execute
+
+ expect(project).not_to be_persisted
+ expect(project).not_to be_valid
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/representation/task_spec.rb b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb
new file mode 100644
index 00000000000..dfbd8c546eb
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/representation/task_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::Representation::Task do
+ subject(:task) do
+ described_class.new(
+ {
+ 'phid' => 'the-phid',
+ 'fields' => {
+ 'name' => 'Title'.ljust(257, '.'), # A string padded to 257 chars
+ 'description' => {
+ 'raw' => '# This is markdown\n it can contain more text.'
+ },
+ 'dateCreated' => '1518688921',
+ 'dateClosed' => '1518789995'
+ }
+ }
+ )
+ end
+
+ describe '#issue_attributes' do
+ it 'contains the expected values' do
+ expected_attributes = {
+ title: 'Title'.ljust(255, '.'),
+ description: '# This is markdown\n it can contain more text.',
+ state: :closed,
+ created_at: Time.at(1518688921),
+ closed_at: Time.at(1518789995)
+ }
+
+ expect(task.issue_attributes).to eq(expected_attributes)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/phabricator_import/worker_state_spec.rb b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
new file mode 100644
index 00000000000..a44947445c9
--- /dev/null
+++ b/spec/lib/gitlab/phabricator_import/worker_state_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe Gitlab::PhabricatorImport::WorkerState, :clean_gitlab_redis_shared_state do
+ subject(:state) { described_class.new('weird-project-id') }
+ let(:key) { 'phabricator-import/jobs/project-weird-project-id/job-count' }
+
+ describe '#add_job' do
+ it 'increments the counter for jobs' do
+ set_value(3)
+
+ expect { state.add_job }.to change { get_value }.from('3').to('4')
+ end
+ end
+
+ describe '#remove_job' do
+ it 'decrements the counter for jobs' do
+ set_value(3)
+
+ expect { state.remove_job }.to change { get_value }.from('3').to('2')
+ end
+ end
+
+ describe '#running_count' do
+ it 'reads the value' do
+ set_value(9)
+
+ expect(state.running_count).to eq(9)
+ end
+
+ it 'returns 0 when nothing was set' do
+ expect(state.running_count).to eq(0)
+ end
+ end
+
+ def set_value(value)
+ redis.with { |r| r.set(key, value) }
+ end
+
+ def get_value
+ redis.with { |r| r.get(key) }
+ end
+
+ def redis
+ Gitlab::Redis::SharedState
+ end
+end
diff --git a/spec/lib/gitlab/profiler_spec.rb b/spec/lib/gitlab/profiler_spec.rb
index 8bb0c1a0b8a..5af52db7a1f 100644
--- a/spec/lib/gitlab/profiler_spec.rb
+++ b/spec/lib/gitlab/profiler_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe Gitlab::Profiler do
- RSpec::Matchers.define_negated_matcher :not_change, :change
-
let(:null_logger) { Logger.new('/dev/null') }
let(:private_token) { 'private' }
@@ -29,13 +27,13 @@ describe Gitlab::Profiler do
it 'sends a POST request when data is passed' do
post_data = '{"a":1}'
- expect(app).to receive(:post).with(anything, post_data, anything)
+ expect(app).to receive(:post).with(anything, params: post_data, headers: anything)
described_class.profile('/', post_data: post_data)
end
it 'uses the private_token for auth if given' do
- expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
+ expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token })
expect(app).to receive(:get).with('/api/v4/users')
described_class.profile('/', private_token: private_token)
@@ -53,7 +51,7 @@ describe Gitlab::Profiler do
user = double(:user)
expect(described_class).to receive(:with_user).with(nil).and_call_original
- expect(app).to receive(:get).with('/', nil, 'Private-Token' => private_token)
+ expect(app).to receive(:get).with('/', params: nil, headers: { 'Private-Token' => private_token })
expect(app).to receive(:get).with('/api/v4/users')
described_class.profile('/', user: user, private_token: private_token)
@@ -187,7 +185,7 @@ describe Gitlab::Profiler do
end
it 'does not modify the standard Rails loggers' do
- expect { described_class.with_custom_logger(nil) { } }
+ expect { described_class.with_custom_logger(nil) {} }
.to not_change { ActiveRecord::Base.logger }
.and not_change { ActionController::Base.logger }
.and not_change { ActiveSupport::LogSubscriber.colorize_logging }
@@ -204,7 +202,7 @@ describe Gitlab::Profiler do
end
it 'cleans up ApplicationController afterwards' do
- expect { described_class.with_user(user) { } }
+ expect { described_class.with_user(user) {} }
.to not_change { ActionController.instance_methods(false) }
end
end
@@ -213,7 +211,7 @@ describe Gitlab::Profiler do
it 'does not define methods on ApplicationController' do
expect(ApplicationController).not_to receive(:define_method)
- described_class.with_user(nil) { }
+ described_class.with_user(nil) {}
end
end
end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 6831274d37c..4a41d5cf51e 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -412,4 +412,36 @@ describe Gitlab::ProjectSearchResults do
end
end
end
+
+ describe 'user search' do
+ it 'returns the user belonging to the project matching the search query' do
+ project = create(:project)
+
+ user1 = create(:user, username: 'gob_bluth')
+ create(:project_member, :developer, user: user1, project: project)
+
+ user2 = create(:user, username: 'michael_bluth')
+ create(:project_member, :developer, user: user2, project: project)
+
+ create(:user, username: 'gob_2018')
+
+ result = described_class.new(user, project, 'gob').objects('users')
+
+ expect(result).to eq [user1]
+ end
+
+ it 'returns the user belonging to the group matching the search query' do
+ group = create(:group)
+ project = create(:project, namespace: group)
+
+ user1 = create(:user, username: 'gob_bluth')
+ create(:group_member, :developer, user: user1, group: group)
+
+ create(:user, username: 'gob_2018')
+
+ result = described_class.new(user, project, 'gob').objects('users')
+
+ expect(result).to eq [user1]
+ end
+ end
end
diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb
index 0cee100b64e..8c2fc048a54 100644
--- a/spec/lib/gitlab/project_template_spec.rb
+++ b/spec/lib/gitlab/project_template_spec.rb
@@ -7,7 +7,10 @@ describe Gitlab::ProjectTemplate do
described_class.new('rails', 'Ruby on Rails', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/rails'),
described_class.new('spring', 'Spring', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/spring'),
described_class.new('express', 'NodeJS Express', 'Includes an MVC structure, .gitignore, Gemfile, and more great stuff', 'https://gitlab.com/gitlab-org/project-templates/express'),
- described_class.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore'),
+ described_class.new('iosswift', 'iOS (Swift)', 'A ready-to-go template for use with iOS Swift apps.', 'https://gitlab.com/gitlab-org/project-templates/iosswift'),
+ described_class.new('dotnetcore', '.NET Core', 'A .NET Core console application template, customizable for any .NET Core project', 'https://gitlab.com/gitlab-org/project-templates/dotnetcore'),
+ described_class.new('android', 'Android', 'A ready-to-go template for use with Android apps.', 'https://gitlab.com/gitlab-org/project-templates/android'),
+ described_class.new('gomicro', 'Go Micro', 'Go Micro is a framework for micro service development.', 'https://gitlab.com/gitlab-org/project-templates/go-micro'),
described_class.new('hugo', 'Pages/Hugo', 'Everything you need to get started using a Hugo Pages site.', 'https://gitlab.com/pages/hugo'),
described_class.new('jekyll', 'Pages/Jekyll', 'Everything you need to get started using a Jekyll Pages site.', 'https://gitlab.com/pages/jekyll'),
described_class.new('plainhtml', 'Pages/Plain HTML', 'Everything you need to get started using a plain HTML Pages site.', 'https://gitlab.com/pages/plain-html'),
diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb
index 5a88b23aa82..a6589f0c0a3 100644
--- a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb
+++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb
@@ -9,9 +9,35 @@ describe Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery do
let(:query_params) { [environment.id] }
it 'queries using specific time' do
- expect(client).to receive(:query_range).with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f)
-
+ expect(client).to receive(:query_range)
+ .with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f)
expect(query_result).not_to be_nil
end
+
+ context 'when start and end time parameters are provided' do
+ let(:query_params) { [environment.id, start_time, end_time] }
+
+ context 'as unix timestamps' do
+ let(:start_time) { 4.hours.ago.to_f }
+ let(:end_time) { 2.hours.ago.to_f }
+
+ it 'queries using the provided times' do
+ expect(client).to receive(:query_range)
+ .with(anything, start: start_time, stop: end_time)
+ expect(query_result).not_to be_nil
+ end
+ end
+
+ context 'as Date/Time objects' do
+ let(:start_time) { 4.hours.ago }
+ let(:end_time) { 2.hours.ago }
+
+ it 'queries using the provided times converted to unix' do
+ expect(client).to receive(:query_range)
+ .with(anything, start: start_time.to_f, stop: end_time.to_f)
+ expect(query_result).not_to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
new file mode 100644
index 00000000000..7f6283715f2
--- /dev/null
+++ b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do
+ include PrometheusHelpers
+
+ let(:project) { create(:project) }
+ let(:serverless_func) { Serverless::Function.new(project, 'test-name', 'test-ns') }
+
+ let(:client) { double('prometheus_client') }
+ subject { described_class.new(client) }
+
+ context 'verify queries' do
+ before do
+ allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns')))
+ allow(client).to receive(:query_range)
+ end
+
+ it 'has the query, but no data' do
+ results = subject.query(serverless_func.id)
+
+ expect(results.queries[0][:query_range]).to eql('floor(sum(rate(istio_revision_request_count{destination_configuration="test-name", destination_namespace="test-ns"}[1m])*30))')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb b/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb
index 420218a695a..936447b8474 100644
--- a/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb
+++ b/spec/lib/gitlab/prometheus/queries/matched_metric_query_spec.rb
@@ -15,7 +15,7 @@ describe Gitlab::Prometheus::Queries::MatchedMetricQuery do
[{ '__name__' => 'metric_a' },
{ '__name__' => 'metric_b' }]
end
- let(:partialy_empty_series_info) { [{ '__name__' => 'metric_a', 'environment' => '' }] }
+ let(:partially_empty_series_info) { [{ '__name__' => 'metric_a', 'environment' => '' }] }
let(:empty_series_info) { [] }
let(:client) { double('prometheus_client') }
@@ -60,7 +60,7 @@ describe Gitlab::Prometheus::Queries::MatchedMetricQuery do
context 'one of the series info was not found' do
before do
- allow(client).to receive(:series).and_return(partialy_empty_series_info)
+ allow(client).to receive(:series).and_return(partially_empty_series_info)
end
it 'responds with one active and one missing metric' do
expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 1 }])
diff --git a/spec/lib/gitlab/prometheus/query_variables_spec.rb b/spec/lib/gitlab/prometheus/query_variables_spec.rb
index 78c74266c61..6dc99ef26ec 100644
--- a/spec/lib/gitlab/prometheus/query_variables_spec.rb
+++ b/spec/lib/gitlab/prometheus/query_variables_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Gitlab::Prometheus::QueryVariables do
describe '.call' do
+ let(:project) { environment.project }
let(:environment) { create(:environment) }
let(:slug) { environment.slug }
@@ -21,13 +22,32 @@ describe Gitlab::Prometheus::QueryVariables do
end
context 'with deployment platform' do
- let(:kube_namespace) { environment.deployment_platform.actual_namespace }
+ context 'with project cluster' do
+ let(:kube_namespace) { environment.deployment_platform.cluster.kubernetes_namespace_for(project) }
- before do
- create(:cluster, :provided_by_user, projects: [environment.project])
+ before do
+ create(:cluster, :project, :provided_by_user, projects: [project])
+ end
+
+ it { is_expected.to include(kube_namespace: kube_namespace) }
end
- it { is_expected.to include(kube_namespace: kube_namespace) }
+ context 'with group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_user, groups: [group]) }
+ let(:group) { create(:group) }
+ let(:project2) { create(:project) }
+ let(:kube_namespace) { k8s_ns.namespace }
+
+ let!(:k8s_ns) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project) }
+ let!(:k8s_ns2) { create(:cluster_kubernetes_namespace, cluster: cluster, project: project2) }
+
+ before do
+ group.projects << project
+ group.projects << project2
+ end
+
+ it { is_expected.to include(kube_namespace: kube_namespace) }
+ end
end
end
end
diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb
index 4c3b8deefb9..f15ae83a02c 100644
--- a/spec/lib/gitlab/prometheus_client_spec.rb
+++ b/spec/lib/gitlab/prometheus_client_spec.rb
@@ -60,15 +60,13 @@ describe Gitlab::PrometheusClient do
end
describe 'failure to reach a provided prometheus url' do
- let(:prometheus_url) {"https://prometheus.invalid.example.com"}
+ let(:prometheus_url) {"https://prometheus.invalid.example.com/api/v1/query?query=1"}
- subject { described_class.new(RestClient::Resource.new(prometheus_url)) }
-
- context 'exceptions are raised' do
+ shared_examples 'exceptions are raised' do
it 'raises a Gitlab::PrometheusClient::Error error when a SocketError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "Can't connect to #{prometheus_url}")
expect(req_stub).to have_been_requested
end
@@ -76,7 +74,7 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusClient::Error error when a SSLError is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "#{prometheus_url} contains invalid SSL data")
expect(req_stub).to have_been_requested
end
@@ -84,11 +82,23 @@ describe Gitlab::PrometheusClient do
it 'raises a Gitlab::PrometheusClient::Error error when a RestClient::Exception is rescued' do
req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception)
- expect { subject.send(:get, '/', {}) }
+ expect { subject }
.to raise_error(Gitlab::PrometheusClient::Error, "Network connection error")
expect(req_stub).to have_been_requested
end
end
+
+ context 'ping' do
+ subject { described_class.new(RestClient::Resource.new(prometheus_url)).ping }
+
+ it_behaves_like 'exceptions are raised'
+ end
+
+ context 'proxy' do
+ subject { described_class.new(RestClient::Resource.new(prometheus_url)).proxy('query', { query: '1' }) }
+
+ it_behaves_like 'exceptions are raised'
+ end
end
describe '#query' do
@@ -230,4 +240,87 @@ describe Gitlab::PrometheusClient do
let(:execute_query) { subject.query_range(prometheus_query) }
end
end
+
+ describe '.compute_step' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:now) { Time.now.utc }
+
+ subject { described_class.compute_step(start, stop) }
+
+ where(:time_interval_in_seconds, :step) do
+ 0 | 60
+ 10.hours | 60
+ 10.hours + 1 | 61
+ # frontend options
+ 30.minutes | 60
+ 3.hours | 60
+ 8.hours | 60
+ 1.day | 144
+ 3.days | 432
+ 1.week | 1008
+ end
+
+ with_them do
+ let(:start) { now - time_interval_in_seconds }
+ let(:stop) { now }
+
+ it { is_expected.to eq(step) }
+ end
+ end
+
+ describe 'proxy' do
+ context 'get API' do
+ let(:prometheus_query) { prometheus_cpu_query('env-slug') }
+ let(:query_url) { prometheus_query_url(prometheus_query) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'when response status code is 200' do
+ it 'returns response object' do
+ req_stub = stub_prometheus_request(query_url, body: prometheus_value_body('vector'))
+
+ response = subject.proxy('query', { query: prometheus_query })
+ json_response = JSON.parse(response.body)
+
+ expect(response.code).to eq(200)
+ expect(json_response).to eq({
+ 'status' => 'success',
+ 'data' => {
+ 'resultType' => 'vector',
+ 'result' => [{ "metric" => {}, "value" => [1488772511.004, "0.000041021495238095323"] }]
+ }
+ })
+ expect(req_stub).to have_been_requested
+ end
+ end
+
+ context 'when response status code is not 200' do
+ it 'returns response object' do
+ req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'error' })
+
+ response = subject.proxy('query', { query: prometheus_query })
+ json_response = JSON.parse(response.body)
+
+ expect(req_stub).to have_been_requested
+ expect(response.code).to eq(400)
+ expect(json_response).to eq('error' => 'error')
+ end
+ end
+
+ context 'when RestClient::Exception is raised' do
+ before do
+ stub_prometheus_request_with_exception(query_url, RestClient::Exception)
+ end
+
+ it 'raises PrometheusClient::Error' do
+ expect { subject.proxy('query', { query: prometheus_query }) }.to(
+ raise_error(Gitlab::PrometheusClient::Error, 'Network connection error')
+ )
+ end
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/push_options_spec.rb b/spec/lib/gitlab/push_options_spec.rb
new file mode 100644
index 00000000000..fc9e421bea6
--- /dev/null
+++ b/spec/lib/gitlab/push_options_spec.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::PushOptions do
+ describe 'namespace and key validation' do
+ it 'ignores unrecognised namespaces' do
+ options = described_class.new(['invalid.key=value'])
+
+ expect(options.get(:invalid)).to eq(nil)
+ end
+
+ it 'ignores unrecognised keys' do
+ options = described_class.new(['merge_request.key=value'])
+
+ expect(options.get(:merge_request)).to eq(nil)
+ end
+
+ it 'ignores blank keys' do
+ options = described_class.new(['merge_request'])
+
+ expect(options.get(:merge_request)).to eq(nil)
+ end
+
+ it 'parses recognised namespace and key pairs' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.get(:merge_request)).to include({
+ target: 'value'
+ })
+ end
+ end
+
+ describe '#get' do
+ it 'can emulate Hash#dig' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+ end
+
+ describe '#as_json' do
+ it 'returns all options' do
+ options = described_class.new(['merge_request.target=value'])
+
+ expect(options.as_json).to include(
+ merge_request: {
+ target: 'value'
+ }
+ )
+ end
+ end
+
+ it 'can parse multiple push options' do
+ options = described_class.new([
+ 'merge_request.create',
+ 'merge_request.target=value'
+ ])
+
+ expect(options.get(:merge_request)).to include({
+ create: true,
+ target: 'value'
+ })
+ expect(options.get(:merge_request, :create)).to eq(true)
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+
+ it 'stores options internally as a HashWithIndifferentAccess' do
+ options = described_class.new([
+ 'merge_request.create'
+ ])
+
+ expect(options.get('merge_request', 'create')).to eq(true)
+ expect(options.get(:merge_request, :create)).to eq(true)
+ end
+
+ it 'selects the last option when options contain duplicate namespace and key pairs' do
+ options = described_class.new([
+ 'merge_request.target=value1',
+ 'merge_request.target=value2'
+ ])
+
+ expect(options.get(:merge_request, :target)).to eq('value2')
+ end
+
+ it 'defaults values to true' do
+ options = described_class.new(['merge_request.create'])
+
+ expect(options.get(:merge_request, :create)).to eq(true)
+ end
+
+ it 'expands aliases' do
+ options = described_class.new(['mr.target=value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+
+ it 'forgives broken push options' do
+ options = described_class.new(['merge_request . target = value'])
+
+ expect(options.get(:merge_request, :target)).to eq('value')
+ end
+end
diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb
index 5dae82a63b4..b6e0adbc1c2 100644
--- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb
+++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb
@@ -69,10 +69,40 @@ describe Gitlab::QuickActions::CommandDefinition do
expect(subject.available?(opts)).to be true
end
end
+
+ context "when the command has types" do
+ before do
+ subject.types = [Issue, Commit]
+ end
+
+ context "when the command target type is allowed" do
+ it "returns true" do
+ opts[:quick_action_target] = Issue.new
+ expect(subject.available?(opts)).to be true
+ end
+ end
+
+ context "when the command target type is not allowed" do
+ it "returns true" do
+ opts[:quick_action_target] = MergeRequest.new
+ expect(subject.available?(opts)).to be false
+ end
+ end
+ end
+
+ context "when the command has no types" do
+ it "any target type is allowed" do
+ opts[:quick_action_target] = Issue.new
+ expect(subject.available?(opts)).to be true
+
+ opts[:quick_action_target] = MergeRequest.new
+ expect(subject.available?(opts)).to be true
+ end
+ end
end
describe "#execute" do
- let(:context) { OpenStruct.new(run: false) }
+ let(:context) { OpenStruct.new(run: false, commands_executed_count: nil) }
context "when the command is a noop" do
it "doesn't execute the command" do
@@ -80,6 +110,7 @@ describe Gitlab::QuickActions::CommandDefinition do
subject.execute(context, nil)
+ expect(context.commands_executed_count).to be_nil
expect(context.run).to be false
end
end
@@ -97,6 +128,7 @@ describe Gitlab::QuickActions::CommandDefinition do
it "doesn't execute the command" do
subject.execute(context, nil)
+ expect(context.commands_executed_count).to be_nil
expect(context.run).to be false
end
end
@@ -112,6 +144,7 @@ describe Gitlab::QuickActions::CommandDefinition do
subject.execute(context, true)
expect(context.run).to be true
+ expect(context.commands_executed_count).to eq(1)
end
end
@@ -120,6 +153,7 @@ describe Gitlab::QuickActions::CommandDefinition do
subject.execute(context, nil)
expect(context.run).to be true
+ expect(context.commands_executed_count).to eq(1)
end
end
end
@@ -134,6 +168,7 @@ describe Gitlab::QuickActions::CommandDefinition do
subject.execute(context, true)
expect(context.run).to be true
+ expect(context.commands_executed_count).to eq(1)
end
end
diff --git a/spec/lib/gitlab/quick_actions/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb
index fd4df8694ba..185adab1ff6 100644
--- a/spec/lib/gitlab/quick_actions/dsl_spec.rb
+++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb
@@ -48,13 +48,19 @@ describe Gitlab::QuickActions::Dsl do
substitution :something do |text|
"#{text} Some complicated thing you want in here"
end
+
+ desc 'A command with types'
+ types Issue, Commit
+ command :has_types do
+ "Has Issue and Commit types"
+ end
end
end
describe '.command_definitions' do
it 'returns an array with commands definitions' do
no_args_def, explanation_with_aliases_def, dynamic_description_def,
- cc_def, cond_action_def, with_params_parsing_def, substitution_def =
+ cc_def, cond_action_def, with_params_parsing_def, substitution_def, has_types =
DummyClass.command_definitions
expect(no_args_def.name).to eq(:no_args)
@@ -63,6 +69,7 @@ describe Gitlab::QuickActions::Dsl do
expect(no_args_def.explanation).to eq('')
expect(no_args_def.params).to eq([])
expect(no_args_def.condition_block).to be_nil
+ expect(no_args_def.types).to eq([])
expect(no_args_def.action_block).to be_a_kind_of(Proc)
expect(no_args_def.parse_params_block).to be_nil
expect(no_args_def.warning).to eq('')
@@ -73,6 +80,7 @@ describe Gitlab::QuickActions::Dsl do
expect(explanation_with_aliases_def.explanation).to eq('Static explanation')
expect(explanation_with_aliases_def.params).to eq(['The first argument'])
expect(explanation_with_aliases_def.condition_block).to be_nil
+ expect(explanation_with_aliases_def.types).to eq([])
expect(explanation_with_aliases_def.action_block).to be_a_kind_of(Proc)
expect(explanation_with_aliases_def.parse_params_block).to be_nil
expect(explanation_with_aliases_def.warning).to eq('Possible problem!')
@@ -83,6 +91,7 @@ describe Gitlab::QuickActions::Dsl do
expect(dynamic_description_def.explanation).to eq('')
expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument'])
expect(dynamic_description_def.condition_block).to be_nil
+ expect(dynamic_description_def.types).to eq([])
expect(dynamic_description_def.action_block).to be_a_kind_of(Proc)
expect(dynamic_description_def.parse_params_block).to be_nil
expect(dynamic_description_def.warning).to eq('')
@@ -93,6 +102,7 @@ describe Gitlab::QuickActions::Dsl do
expect(cc_def.explanation).to eq('')
expect(cc_def.params).to eq([])
expect(cc_def.condition_block).to be_nil
+ expect(cc_def.types).to eq([])
expect(cc_def.action_block).to be_nil
expect(cc_def.parse_params_block).to be_nil
expect(cc_def.warning).to eq('')
@@ -103,6 +113,7 @@ describe Gitlab::QuickActions::Dsl do
expect(cond_action_def.explanation).to be_a_kind_of(Proc)
expect(cond_action_def.params).to eq([])
expect(cond_action_def.condition_block).to be_a_kind_of(Proc)
+ expect(cond_action_def.types).to eq([])
expect(cond_action_def.action_block).to be_a_kind_of(Proc)
expect(cond_action_def.parse_params_block).to be_nil
expect(cond_action_def.warning).to eq('')
@@ -113,6 +124,7 @@ describe Gitlab::QuickActions::Dsl do
expect(with_params_parsing_def.explanation).to eq('')
expect(with_params_parsing_def.params).to eq([])
expect(with_params_parsing_def.condition_block).to be_nil
+ expect(with_params_parsing_def.types).to eq([])
expect(with_params_parsing_def.action_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.parse_params_block).to be_a_kind_of(Proc)
expect(with_params_parsing_def.warning).to eq('')
@@ -123,9 +135,21 @@ describe Gitlab::QuickActions::Dsl do
expect(substitution_def.explanation).to eq('')
expect(substitution_def.params).to eq(['<Comment>'])
expect(substitution_def.condition_block).to be_nil
+ expect(substitution_def.types).to eq([])
expect(substitution_def.action_block.call('text')).to eq('text Some complicated thing you want in here')
expect(substitution_def.parse_params_block).to be_nil
expect(substitution_def.warning).to eq('')
+
+ expect(has_types.name).to eq(:has_types)
+ expect(has_types.aliases).to eq([])
+ expect(has_types.description).to eq('A command with types')
+ expect(has_types.explanation).to eq('')
+ expect(has_types.params).to eq([])
+ expect(has_types.condition_block).to be_nil
+ expect(has_types.types).to eq([Issue, Commit])
+ expect(has_types.action_block).to be_a_kind_of(Proc)
+ expect(has_types.parse_params_block).to be_nil
+ expect(has_types.warning).to eq('')
end
end
end
diff --git a/spec/lib/gitlab/rack_timeout_observer_spec.rb b/spec/lib/gitlab/rack_timeout_observer_spec.rb
new file mode 100644
index 00000000000..3dc1a8b68fb
--- /dev/null
+++ b/spec/lib/gitlab/rack_timeout_observer_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::RackTimeoutObserver do
+ let(:counter) { Gitlab::Metrics::NullMetric.instance }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(any_args)
+ .and_return(counter)
+ end
+
+ describe '#callback' do
+ context 'when request times out' do
+ let(:env) do
+ {
+ ::Rack::Timeout::ENV_INFO_KEY => double(state: :timed_out),
+ 'action_dispatch.request.parameters' => {
+ 'controller' => 'foo',
+ 'action' => 'bar'
+ }
+ }
+ end
+
+ subject { described_class.new }
+
+ it 'increments timeout counter' do
+ expect(counter)
+ .to receive(:increment)
+ .with({ controller: 'foo', action: 'bar', route: nil, state: :timed_out })
+
+ subject.callback.call(env)
+ end
+ end
+
+ context 'when request expires' do
+ let(:endpoint) { double }
+ let(:env) do
+ {
+ ::Rack::Timeout::ENV_INFO_KEY => double(state: :expired),
+ Grape::Env::API_ENDPOINT => endpoint
+ }
+ end
+
+ subject { described_class.new }
+
+ it 'increments timeout counter' do
+ allow(endpoint).to receive_message_chain('route.pattern.origin') { 'foobar' }
+ expect(counter)
+ .to receive(:increment)
+ .with({ controller: nil, action: nil, route: 'foobar', state: :expired })
+
+ subject.callback.call(env)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 4139d1c650c..d982053d92e 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::ReferenceExtractor do
diff --git a/spec/lib/gitlab/repo_path_spec.rb b/spec/lib/gitlab/repo_path_spec.rb
index 13940713dfc..8fbda929064 100644
--- a/spec/lib/gitlab/repo_path_spec.rb
+++ b/spec/lib/gitlab/repo_path_spec.rb
@@ -6,43 +6,49 @@ describe ::Gitlab::RepoPath do
context 'a repository storage path' do
it 'parses a full repository path' do
- expect(described_class.parse(project.repository.full_path)).to eq([project, false, nil])
+ expect(described_class.parse(project.repository.full_path)).to eq([project, Gitlab::GlRepository::PROJECT, nil])
end
it 'parses a full wiki path' do
- expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, true, nil])
+ expect(described_class.parse(project.wiki.repository.full_path)).to eq([project, Gitlab::GlRepository::WIKI, nil])
end
end
context 'a relative path' do
it 'parses a relative repository path' do
- expect(described_class.parse(project.full_path + '.git')).to eq([project, false, nil])
+ expect(described_class.parse(project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil])
end
it 'parses a relative wiki path' do
- expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, true, nil])
+ expect(described_class.parse(project.full_path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, nil])
end
it 'parses a relative path starting with /' do
- expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, false, nil])
+ expect(described_class.parse('/' + project.full_path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, nil])
end
context 'of a redirected project' do
let(:redirect) { project.route.create_redirect('foo/bar') }
it 'parses a relative repository path' do
- expect(described_class.parse(redirect.path + '.git')).to eq([project, false, 'foo/bar'])
+ expect(described_class.parse(redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar'])
end
it 'parses a relative wiki path' do
- expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, true, 'foo/bar.wiki'])
+ expect(described_class.parse(redirect.path + '.wiki.git')).to eq([project, Gitlab::GlRepository::WIKI, 'foo/bar.wiki'])
end
it 'parses a relative path starting with /' do
- expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, false, 'foo/bar'])
+ expect(described_class.parse('/' + redirect.path + '.git')).to eq([project, Gitlab::GlRepository::PROJECT, 'foo/bar'])
end
end
end
+
+ it "returns the default type for non existent paths" do
+ _project, type, _redirected = described_class.parse("path/non-existent.git")
+
+ expect(type).to eq(Gitlab::GlRepository.default_type)
+ end
end
describe '.find_project' do
diff --git a/spec/lib/gitlab/request_context_spec.rb b/spec/lib/gitlab/request_context_spec.rb
index fd443cc1f71..23e45aff1c5 100644
--- a/spec/lib/gitlab/request_context_spec.rb
+++ b/spec/lib/gitlab/request_context_spec.rb
@@ -6,6 +6,31 @@ describe Gitlab::RequestContext do
let(:app) { -> (env) {} }
let(:env) { Hash.new }
+ context 'with X-Forwarded-For headers', :request_store do
+ let(:load_balancer_ip) { '1.2.3.4' }
+ let(:headers) do
+ {
+ 'HTTP_X_FORWARDED_FOR' => "#{load_balancer_ip}, 127.0.0.1",
+ 'REMOTE_ADDR' => '127.0.0.1'
+ }
+ end
+
+ let(:env) { Rack::MockRequest.env_for("/").merge(headers) }
+
+ it 'returns the load balancer IP' do
+ client_ip = nil
+
+ endpoint = proc do
+ client_ip = Gitlab::SafeRequestStore[:client_ip]
+ [200, {}, ["Hello"]]
+ end
+
+ described_class.new(endpoint).call(env)
+
+ expect(client_ip).to eq(load_balancer_ip)
+ end
+ end
+
context 'when RequestStore::Middleware is used' do
around do |example|
RequestStore::Middleware.new(-> (env) { example.run }).call({})
@@ -15,7 +40,7 @@ describe Gitlab::RequestContext do
let(:ip) { '192.168.1.11' }
before do
- allow_any_instance_of(ActionDispatch::Request).to receive(:ip).and_return(ip)
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
described_class.new(app).call(env)
end
diff --git a/spec/lib/gitlab/route_map_spec.rb b/spec/lib/gitlab/route_map_spec.rb
index d672f7b5675..a39c774429e 100644
--- a/spec/lib/gitlab/route_map_spec.rb
+++ b/spec/lib/gitlab/route_map_spec.rb
@@ -60,7 +60,7 @@ describe Gitlab::RouteMap do
subject do
map = described_class.new(<<-"MAP".strip_heredoc)
- - source: '#{malicious_regexp}'
+ - source: '#{malicious_regexp_re2}'
public: '/'
MAP
diff --git a/spec/lib/gitlab/sanitizers/exif_spec.rb b/spec/lib/gitlab/sanitizers/exif_spec.rb
new file mode 100644
index 00000000000..bd5f330c7a1
--- /dev/null
+++ b/spec/lib/gitlab/sanitizers/exif_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Gitlab::Sanitizers::Exif do
+ let(:sanitizer) { described_class.new }
+
+ describe '#batch_clean' do
+ context 'with image uploads' do
+ let!(:uploads) { create_list(:upload, 3, :with_file, :issuable_upload) }
+
+ it 'processes all uploads if range ID is not set' do
+ expect(sanitizer).to receive(:clean).exactly(3).times
+
+ sanitizer.batch_clean
+ end
+
+ it 'processes only uploads in the selected range' do
+ expect(sanitizer).to receive(:clean).once
+
+ sanitizer.batch_clean(start_id: uploads[1].id, stop_id: uploads[1].id)
+ end
+
+ it 'pauses if sleep_time is set' do
+ expect(sanitizer).to receive(:sleep).exactly(3).times.with(1.second)
+ expect(sanitizer).to receive(:clean).exactly(3).times
+
+ sanitizer.batch_clean(sleep_time: 1)
+ end
+ end
+
+ it 'filters only jpg/tiff images' do
+ create(:upload, path: 'filename.jpg')
+ create(:upload, path: 'filename.jpeg')
+ create(:upload, path: 'filename.JPG')
+ create(:upload, path: 'filename.tiff')
+ create(:upload, path: 'filename.TIFF')
+ create(:upload, path: 'filename.png')
+ create(:upload, path: 'filename.txt')
+
+ expect(sanitizer).to receive(:clean).exactly(5).times
+ sanitizer.batch_clean
+ end
+ end
+
+ describe '#clean' do
+ let(:uploader) { create(:upload, :with_file, :issuable_upload).build_uploader }
+
+ context "no dry run" do
+ it "removes exif from the image" do
+ uploader.store!(fixture_file_upload('spec/fixtures/rails_sample.jpg'))
+
+ original_upload = uploader.upload
+ expected_args = ["exiftool", "-all=", "-tagsFromFile", "@", *Gitlab::Sanitizers::Exif::EXCLUDE_PARAMS, "--IPTC:all", "--XMP-iptcExt:all", kind_of(String)]
+
+ expect(sanitizer).to receive(:extra_tags).and_return(["", 0])
+ expect(sanitizer).to receive(:exec_remove_exif!).once.and_call_original
+ expect(uploader).to receive(:store!).and_call_original
+ expect(Gitlab::Popen).to receive(:popen).with(expected_args) do |args|
+ File.write("#{args.last}_original", "foo") if args.last.start_with?(Dir.tmpdir)
+
+ [expected_args, 0]
+ end
+
+ sanitizer.clean(uploader, dry_run: false)
+
+ expect(uploader.upload.id).not_to eq(original_upload.id)
+ expect(uploader.upload.path).to eq(original_upload.path)
+ end
+
+ it "ignores image without exif" do
+ expected_args = ["exiftool", "-all", "-j", "-sort", "--IPTC:all", "--XMP-iptcExt:all", kind_of(String)]
+
+ expect(Gitlab::Popen).to receive(:popen).with(expected_args).and_return(["[{}]", 0])
+ expect(sanitizer).not_to receive(:exec_remove_exif!)
+ expect(uploader).not_to receive(:store!)
+
+ sanitizer.clean(uploader, dry_run: false)
+ end
+
+ it "raises an error if the exiftool fails with an error" do
+ expect(Gitlab::Popen).to receive(:popen).and_return(["error", 1])
+
+ expect { sanitizer.clean(uploader, dry_run: false) }.to raise_exception(RuntimeError, "failed to get exif tags: error")
+ end
+ end
+
+ context "dry run" do
+ it "doesn't change the image" do
+ expect(sanitizer).to receive(:extra_tags).and_return({ 'foo' => 'bar' })
+ expect(sanitizer).not_to receive(:exec_remove_exif!)
+ expect(uploader).not_to receive(:store!)
+
+ sanitizer.clean(uploader, dry_run: true)
+ end
+ end
+ end
+
+ describe "#extra_tags" do
+ it "returns a list of keys for exif file" do
+ tags = '[{
+ "DigitalSourceType": "some source",
+ "ImageHeight": 654
+ }]'
+
+ expect(Gitlab::Popen).to receive(:popen).and_return([tags, 0])
+
+ expect(sanitizer.extra_tags('filename')).not_to be_empty
+ end
+
+ it "returns an empty list for file with only whitelisted and ignored tags" do
+ tags = '[{
+ "ImageHeight": 654,
+ "Megapixels": 0.641
+ }]'
+
+ expect(Gitlab::Popen).to receive(:popen).and_return([tags, 0])
+
+ expect(sanitizer.extra_tags('some file')).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 87288baedb0..3d27156b356 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -97,7 +97,7 @@ describe Gitlab::SearchResults do
results.objects('merge_requests')
end
- it 'it skips project filter if default project context is used' do
+ it 'skips project filter if default project context is used' do
allow(results).to receive(:default_project_filter).and_return(true)
expect(results).not_to receive(:project_ids_relation)
@@ -113,7 +113,7 @@ describe Gitlab::SearchResults do
results.objects('issues')
end
- it 'it skips project filter if default project context is used' do
+ it 'skips project filter if default project context is used' do
allow(results).to receive(:default_project_filter).and_return(true)
expect(results).not_to receive(:project_ids_relation)
@@ -121,6 +121,22 @@ describe Gitlab::SearchResults do
results.objects('issues')
end
end
+
+ describe '#users' do
+ it 'does not call the UsersFinder when the current_user is not allowed to read users list' do
+ allow(Ability).to receive(:allowed?).and_return(false)
+
+ expect(UsersFinder).not_to receive(:new).with(user, search: 'foo').and_call_original
+
+ results.objects('users')
+ end
+
+ it 'calls the UsersFinder' do
+ expect(UsersFinder).to receive(:new).with(user, search: 'foo').and_call_original
+
+ results.objects('users')
+ end
+ end
end
it 'does not list issues on private projects' do
@@ -240,4 +256,28 @@ describe Gitlab::SearchResults do
expect(results.objects('merge_requests')).not_to include merge_request
end
+
+ context 'milestones' do
+ it 'returns correct set of milestones' do
+ private_project_1 = create(:project, :private)
+ private_project_2 = create(:project, :private)
+ internal_project = create(:project, :internal)
+ public_project_1 = create(:project, :public)
+ public_project_2 = create(:project, :public, :issues_disabled, :merge_requests_disabled)
+ private_project_1.add_developer(user)
+ # milestones that should not be visible
+ create(:milestone, project: private_project_2, title: 'Private project without access milestone')
+ create(:milestone, project: public_project_2, title: 'Public project with milestones disabled milestone')
+ # milestones that should be visible
+ milestone_1 = create(:milestone, project: private_project_1, title: 'Private project with access milestone', state: 'closed')
+ milestone_2 = create(:milestone, project: internal_project, title: 'Internal project milestone')
+ milestone_3 = create(:milestone, project: public_project_1, title: 'Public project with milestones enabled milestone')
+ # Global search scope takes user authorized projects, internal projects and public projects.
+ limit_projects = ProjectsFinder.new(current_user: user).execute
+
+ milestones = described_class.new(user, limit_projects, 'milestone').objects('milestones')
+
+ expect(milestones).to match_array([milestone_1, milestone_2, milestone_3])
+ end
+ end
end
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index 1128eaf8560..af8b059b984 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -2,12 +2,15 @@ require 'spec_helper'
describe Gitlab::Sentry do
describe '.context' do
- it 'adds the locale to the tags' do
+ it 'adds the expected tags' do
expect(described_class).to receive(:enabled?).and_return(true)
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
described_class.context(nil)
expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s)
+ expect(Raven.tags_context[Labkit::Correlation::CorrelationId::LOG_KEY.to_sym].to_s)
+ .to eq('cid')
end
end
@@ -27,7 +30,7 @@ describe Gitlab::Sentry do
context 'when exceptions should not be raised' do
before do
allow(described_class).to receive(:should_raise_for_dev?).and_return(false)
- allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid')
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
end
it 'logs the exception with all attributes passed' do
@@ -65,7 +68,7 @@ describe Gitlab::Sentry do
before do
allow(described_class).to receive(:enabled?).and_return(true)
- allow(Gitlab::CorrelationId).to receive(:current_id).and_return('cid')
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
end
it 'calls Raven.capture_exception' do
diff --git a/spec/lib/gitlab/session_spec.rb b/spec/lib/gitlab/session_spec.rb
new file mode 100644
index 00000000000..8db73f0ec7b
--- /dev/null
+++ b/spec/lib/gitlab/session_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Session do
+ it 'uses the current thread as a data store' do
+ Thread.current[:session_storage] = { a: :b }
+
+ expect(described_class.current).to eq(a: :b)
+ ensure
+ Thread.current[:session_storage] = nil
+ end
+
+ describe '#with_session' do
+ it 'sets session hash' do
+ described_class.with_session(one: 1) do
+ expect(described_class.current).to eq(one: 1)
+ end
+ end
+
+ it 'restores current store after' do
+ described_class.with_session(two: 2) { }
+
+ expect(described_class.current).to eq nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index 033e1bf81a1..bce2e754176 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -8,6 +8,7 @@ describe Gitlab::Shell do
let(:gitlab_shell) { described_class.new }
let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } }
let(:timeout) { Gitlab.config.gitlab_shell.git_timeout }
+ let(:gitlab_authorized_keys) { double }
before do
allow(Project).to receive(:find).and_return(project)
@@ -49,13 +50,38 @@ describe Gitlab::Shell do
describe '#add_key' do
context 'when authorized_keys_enabled is true' do
- it 'removes trailing garbage' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
- )
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ allow(gitlab_shell)
+ .to receive(:gitlab_shell_keys_path)
+ .and_return(:gitlab_shell_keys_path)
+ end
- gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ it 'calls #gitlab_shell_fast_execute with add-key command' do
+ expect(gitlab_shell)
+ .to receive(:gitlab_shell_fast_execute)
+ .with([
+ :gitlab_shell_keys_path,
+ 'add-key',
+ 'key-123',
+ 'ssh-rsa foobar'
+ ])
+
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ end
+ end
+
+ context 'authorized_keys_file set' do
+ it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do
+ expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys)
+
+ expect(gitlab_authorized_keys)
+ .to receive(:add_key)
+ .with('key-123', 'ssh-rsa foobar')
+
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar')
+ end
end
end
@@ -64,10 +90,24 @@ describe Gitlab::Shell do
stub_application_setting(authorized_keys_enabled: false)
end
- it 'does nothing' do
- expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute)
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ end
+
+ it 'does nothing' do
+ expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute)
- gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ end
+ end
+
+ context 'authorized_keys_file set' do
+ it 'does nothing' do
+ expect(Gitlab::AuthorizedKeys).not_to receive(:new)
+
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ end
end
end
@@ -76,24 +116,89 @@ describe Gitlab::Shell do
stub_application_setting(authorized_keys_enabled: nil)
end
- it 'removes trailing garbage' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [:gitlab_shell_keys_path, 'add-key', 'key-123', 'ssh-rsa foobar']
- )
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ allow(gitlab_shell)
+ .to receive(:gitlab_shell_keys_path)
+ .and_return(:gitlab_shell_keys_path)
+ end
+
+ it 'calls #gitlab_shell_fast_execute with add-key command' do
+ expect(gitlab_shell)
+ .to receive(:gitlab_shell_fast_execute)
+ .with([
+ :gitlab_shell_keys_path,
+ 'add-key',
+ 'key-123',
+ 'ssh-rsa foobar'
+ ])
+
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ end
+ end
+
+ context 'authorized_keys_file set' do
+ it 'calls Gitlab::AuthorizedKeys#add_key with id and key' do
+ expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys)
- gitlab_shell.add_key('key-123', 'ssh-rsa foobar trailing garbage')
+ expect(gitlab_authorized_keys)
+ .to receive(:add_key)
+ .with('key-123', 'ssh-rsa foobar')
+
+ gitlab_shell.add_key('key-123', 'ssh-rsa foobar')
+ end
end
end
end
describe '#batch_add_keys' do
+ let(:keys) { [double(shell_id: 'key-123', key: 'ssh-rsa foobar')] }
+
context 'when authorized_keys_enabled is true' do
- it 'instantiates KeyAdder' do
- expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar')
+ context 'authorized_keys_file not set' do
+ let(:io) { double }
- gitlab_shell.batch_add_keys do |adder|
- adder.add_key('key-123', 'ssh-rsa foobar')
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ end
+
+ context 'valid keys' do
+ before do
+ allow(gitlab_shell)
+ .to receive(:gitlab_shell_keys_path)
+ .and_return(:gitlab_shell_keys_path)
+ end
+
+ it 'calls gitlab-keys with batch-add-keys command' do
+ expect(IO)
+ .to receive(:popen)
+ .with("gitlab_shell_keys_path batch-add-keys", 'w')
+ .and_yield(io)
+
+ expect(io).to receive(:puts).with("key-123\tssh-rsa foobar")
+ expect(gitlab_shell.batch_add_keys(keys)).to be_truthy
+ end
+ end
+
+ context 'invalid keys' do
+ let(:keys) { [double(shell_id: 'key-123', key: "ssh-rsa A\tSDFA\nSGADG")] }
+
+ it 'catches failure and returns false' do
+ expect(gitlab_shell.batch_add_keys(keys)).to be_falsey
+ end
+ end
+ end
+
+ context 'authorized_keys_file set' do
+ it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do
+ expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys)
+
+ expect(gitlab_authorized_keys)
+ .to receive(:batch_add_keys)
+ .with(keys)
+
+ gitlab_shell.batch_add_keys(keys)
end
end
end
@@ -103,11 +208,23 @@ describe Gitlab::Shell do
stub_application_setting(authorized_keys_enabled: false)
end
- it 'does nothing' do
- expect_any_instance_of(Gitlab::Shell::KeyAdder).not_to receive(:add_key)
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ end
+
+ it 'does nothing' do
+ expect(IO).not_to receive(:popen)
+
+ gitlab_shell.batch_add_keys(keys)
+ end
+ end
+
+ context 'authorized_keys_file set' do
+ it 'does nothing' do
+ expect(Gitlab::AuthorizedKeys).not_to receive(:new)
- gitlab_shell.batch_add_keys do |adder|
- adder.add_key('key-123', 'ssh-rsa foobar')
+ gitlab_shell.batch_add_keys(keys)
end
end
end
@@ -117,11 +234,37 @@ describe Gitlab::Shell do
stub_application_setting(authorized_keys_enabled: nil)
end
- it 'instantiates KeyAdder' do
- expect_any_instance_of(Gitlab::Shell::KeyAdder).to receive(:add_key).with('key-123', 'ssh-rsa foobar')
+ context 'authorized_keys_file not set' do
+ let(:io) { double }
- gitlab_shell.batch_add_keys do |adder|
- adder.add_key('key-123', 'ssh-rsa foobar')
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ allow(gitlab_shell)
+ .to receive(:gitlab_shell_keys_path)
+ .and_return(:gitlab_shell_keys_path)
+ end
+
+ it 'calls gitlab-keys with batch-add-keys command' do
+ expect(IO)
+ .to receive(:popen)
+ .with("gitlab_shell_keys_path batch-add-keys", 'w')
+ .and_yield(io)
+
+ expect(io).to receive(:puts).with("key-123\tssh-rsa foobar")
+
+ gitlab_shell.batch_add_keys(keys)
+ end
+ end
+
+ context 'authorized_keys_file set' do
+ it 'calls Gitlab::AuthorizedKeys#batch_add_keys with keys to be added' do
+ expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys)
+
+ expect(gitlab_authorized_keys)
+ .to receive(:batch_add_keys)
+ .with(keys)
+
+ gitlab_shell.batch_add_keys(keys)
end
end
end
@@ -129,13 +272,34 @@ describe Gitlab::Shell do
describe '#remove_key' do
context 'when authorized_keys_enabled is true' do
- it 'removes trailing garbage' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar']
- )
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ allow(gitlab_shell)
+ .to receive(:gitlab_shell_keys_path)
+ .and_return(:gitlab_shell_keys_path)
+ end
+
+ it 'calls #gitlab_shell_fast_execute with rm-key command' do
+ expect(gitlab_shell)
+ .to receive(:gitlab_shell_fast_execute)
+ .with([
+ :gitlab_shell_keys_path,
+ 'rm-key',
+ 'key-123'
+ ])
- gitlab_shell.remove_key('key-123', 'ssh-rsa foobar')
+ gitlab_shell.remove_key('key-123')
+ end
+ end
+
+ context 'authorized_keys_file not set' do
+ it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do
+ expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys)
+ expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123')
+
+ gitlab_shell.remove_key('key-123')
+ end
end
end
@@ -144,10 +308,24 @@ describe Gitlab::Shell do
stub_application_setting(authorized_keys_enabled: false)
end
- it 'does nothing' do
- expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute)
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ end
+
+ it 'does nothing' do
+ expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute)
+
+ gitlab_shell.remove_key('key-123')
+ end
+ end
+
+ context 'authorized_keys_file set' do
+ it 'does nothing' do
+ expect(Gitlab::AuthorizedKeys).not_to receive(:new)
- gitlab_shell.remove_key('key-123', 'ssh-rsa foobar')
+ gitlab_shell.remove_key('key-123')
+ end
end
end
@@ -156,232 +334,256 @@ describe Gitlab::Shell do
stub_application_setting(authorized_keys_enabled: nil)
end
- it 'removes trailing garbage' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [:gitlab_shell_keys_path, 'rm-key', 'key-123', 'ssh-rsa foobar']
- )
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ allow(gitlab_shell)
+ .to receive(:gitlab_shell_keys_path)
+ .and_return(:gitlab_shell_keys_path)
+ end
+
+ it 'calls #gitlab_shell_fast_execute with rm-key command' do
+ expect(gitlab_shell)
+ .to receive(:gitlab_shell_fast_execute)
+ .with([
+ :gitlab_shell_keys_path,
+ 'rm-key',
+ 'key-123'
+ ])
- gitlab_shell.remove_key('key-123', 'ssh-rsa foobar')
+ gitlab_shell.remove_key('key-123')
+ end
end
- end
- context 'when key content is not given' do
- it 'calls rm-key with only one argument' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [:gitlab_shell_keys_path, 'rm-key', 'key-123']
- )
+ context 'authorized_keys_file not set' do
+ it 'calls Gitlab::AuthorizedKeys#rm_key with the key to be removed' do
+ expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys)
+ expect(gitlab_authorized_keys).to receive(:rm_key).with('key-123')
- gitlab_shell.remove_key('key-123')
+ gitlab_shell.remove_key('key-123')
+ end
end
end
end
describe '#remove_all_keys' do
context 'when authorized_keys_enabled is true' do
- it 'removes trailing garbage' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with([:gitlab_shell_keys_path, 'clear'])
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ allow(gitlab_shell)
+ .to receive(:gitlab_shell_keys_path)
+ .and_return(:gitlab_shell_keys_path)
+ end
- gitlab_shell.remove_all_keys
- end
- end
+ it 'calls #gitlab_shell_fast_execute with clear command' do
+ expect(gitlab_shell)
+ .to receive(:gitlab_shell_fast_execute)
+ .with([:gitlab_shell_keys_path, 'clear'])
- context 'when authorized_keys_enabled is false' do
- before do
- stub_application_setting(authorized_keys_enabled: false)
+ gitlab_shell.remove_all_keys
+ end
end
- it 'does nothing' do
- expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute)
+ context 'authorized_keys_file set' do
+ it 'calls Gitlab::AuthorizedKeys#clear' do
+ expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys)
+ expect(gitlab_authorized_keys).to receive(:clear)
- gitlab_shell.remove_all_keys
+ gitlab_shell.remove_all_keys
+ end
end
end
- context 'when authorized_keys_enabled is nil' do
+ context 'when authorized_keys_enabled is false' do
before do
- stub_application_setting(authorized_keys_enabled: nil)
+ stub_application_setting(authorized_keys_enabled: false)
end
- it 'removes trailing garbage' do
- allow(gitlab_shell).to receive(:gitlab_shell_keys_path).and_return(:gitlab_shell_keys_path)
- expect(gitlab_shell).to receive(:gitlab_shell_fast_execute).with(
- [:gitlab_shell_keys_path, 'clear']
- )
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ end
- gitlab_shell.remove_all_keys
- end
- end
- end
+ it 'does nothing' do
+ expect(gitlab_shell).not_to receive(:gitlab_shell_fast_execute)
- describe '#remove_keys_not_found_in_db' do
- context 'when keys are in the file that are not in the DB' do
- before do
- gitlab_shell.remove_all_keys
- gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
- gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF')
- @another_key = create(:key) # this one IS in the DB
+ gitlab_shell.remove_all_keys
+ end
end
- it 'removes the keys' do
- expect(find_in_authorized_keys_file(1234)).to be_truthy
- expect(find_in_authorized_keys_file(9876)).to be_truthy
- expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy
- gitlab_shell.remove_keys_not_found_in_db
- expect(find_in_authorized_keys_file(1234)).to be_falsey
- expect(find_in_authorized_keys_file(9876)).to be_falsey
- expect(find_in_authorized_keys_file(@another_key.id)).to be_truthy
+ context 'authorized_keys_file set' do
+ it 'does nothing' do
+ expect(Gitlab::AuthorizedKeys).not_to receive(:new)
+
+ gitlab_shell.remove_all_keys
+ end
end
end
- context 'when keys there are duplicate keys in the file that are not in the DB' do
+ context 'when authorized_keys_enabled is nil' do
before do
- gitlab_shell.remove_all_keys
- gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
- gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
+ stub_application_setting(authorized_keys_enabled: nil)
end
- it 'removes the keys' do
- expect(find_in_authorized_keys_file(1234)).to be_truthy
- gitlab_shell.remove_keys_not_found_in_db
- expect(find_in_authorized_keys_file(1234)).to be_falsey
- end
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ allow(gitlab_shell)
+ .to receive(:gitlab_shell_keys_path)
+ .and_return(:gitlab_shell_keys_path)
+ end
- it 'does not run remove more than once per key (in a batch)' do
- expect(gitlab_shell).to receive(:remove_key).with('key-1234').once
- gitlab_shell.remove_keys_not_found_in_db
- end
- end
+ it 'calls #gitlab_shell_fast_execute with clear command' do
+ expect(gitlab_shell)
+ .to receive(:gitlab_shell_fast_execute)
+ .with([:gitlab_shell_keys_path, 'clear'])
- context 'when keys there are duplicate keys in the file that ARE in the DB' do
- before do
- gitlab_shell.remove_all_keys
- @key = create(:key)
- gitlab_shell.add_key(@key.shell_id, @key.key)
+ gitlab_shell.remove_all_keys
+ end
end
- it 'does not remove the key' do
- gitlab_shell.remove_keys_not_found_in_db
- expect(find_in_authorized_keys_file(@key.id)).to be_truthy
- end
+ context 'authorized_keys_file set' do
+ it 'calls Gitlab::AuthorizedKeys#clear' do
+ expect(Gitlab::AuthorizedKeys).to receive(:new).and_return(gitlab_authorized_keys)
+ expect(gitlab_authorized_keys).to receive(:clear)
- it 'does not need to run a SELECT query for that batch, on account of that key' do
- expect_any_instance_of(ActiveRecord::Relation).not_to receive(:pluck)
- gitlab_shell.remove_keys_not_found_in_db
+ gitlab_shell.remove_all_keys
+ end
end
end
+ end
- unless ENV['CI'] # Skip in CI, it takes 1 minute
- context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do
+ describe '#remove_keys_not_found_in_db' do
+ context 'when keys are in the file that are not in the DB' do
+ context 'authorized_keys_file not set' do
before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
gitlab_shell.remove_all_keys
- 100.times { |i| create(:key) } # first batch is all in the DB
gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
+ gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF')
+ @another_key = create(:key) # this one IS in the DB
end
- it 'removes the keys not in the DB' do
- expect(find_in_authorized_keys_file(1234)).to be_truthy
+ it 'removes the keys' do
+ expect(gitlab_shell).to receive(:remove_key).with('key-1234')
+ expect(gitlab_shell).to receive(:remove_key).with('key-9876')
+ expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}")
+
gitlab_shell.remove_keys_not_found_in_db
- expect(find_in_authorized_keys_file(1234)).to be_falsey
end
end
- end
- end
- describe '#batch_read_key_ids' do
- context 'when there are keys in the authorized_keys file' do
- before do
- gitlab_shell.remove_all_keys
- (1..4).each do |i|
- gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}")
+ context 'authorized_keys_file set' do
+ before do
+ gitlab_shell.remove_all_keys
+ gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
+ gitlab_shell.add_key('key-9876', 'ssh-rsa ASDFASDF')
+ @another_key = create(:key) # this one IS in the DB
end
- end
- it 'iterates over the key IDs in the file, in batches' do
- loop_count = 0
- first_batch = [1, 2]
- second_batch = [3, 4]
+ it 'removes the keys' do
+ expect(gitlab_shell).to receive(:remove_key).with('key-1234')
+ expect(gitlab_shell).to receive(:remove_key).with('key-9876')
+ expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@another_key.id}")
- gitlab_shell.batch_read_key_ids(batch_size: 2) do |batch|
- expected = (loop_count == 0 ? first_batch : second_batch)
- expect(batch).to eq(expected)
- loop_count += 1
+ gitlab_shell.remove_keys_not_found_in_db
end
end
end
- end
- describe '#list_key_ids' do
- context 'when there are keys in the authorized_keys file' do
- before do
- gitlab_shell.remove_all_keys
- (1..4).each do |i|
- gitlab_shell.add_key("key-#{i}", "ssh-rsa ASDFASDF#{i}")
+ context 'when keys there are duplicate keys in the file that are not in the DB' do
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ gitlab_shell.remove_all_keys
+ gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
+ gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
+ end
+
+ it 'removes the keys' do
+ expect(gitlab_shell).to receive(:remove_key).with('key-1234')
+
+ gitlab_shell.remove_keys_not_found_in_db
end
end
- it 'outputs the key IDs in the file, separated by newlines' do
- ids = []
- gitlab_shell.list_key_ids do |io|
- io.each do |line|
- ids << line
- end
+ context 'authorized_keys_file set' do
+ before do
+ gitlab_shell.remove_all_keys
+ gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
+ gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
end
- expect(ids).to eq(%W{1\n 2\n 3\n 4\n})
- end
- end
+ it 'removes the keys' do
+ expect(gitlab_shell).to receive(:remove_key).with('key-1234')
- context 'when there are no keys in the authorized_keys file' do
- before do
- gitlab_shell.remove_all_keys
+ gitlab_shell.remove_keys_not_found_in_db
+ end
end
+ end
- it 'outputs nothing, not even an empty string' do
- ids = []
- gitlab_shell.list_key_ids do |io|
- io.each do |line|
- ids << line
- end
+ context 'when keys there are duplicate keys in the file that ARE in the DB' do
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ gitlab_shell.remove_all_keys
+ @key = create(:key)
+ gitlab_shell.add_key(@key.shell_id, @key.key)
end
- expect(ids).to eq([])
+ it 'does not remove the key' do
+ expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}")
+
+ gitlab_shell.remove_keys_not_found_in_db
+ end
end
- end
- end
- describe Gitlab::Shell::KeyAdder do
- describe '#add_key' do
- it 'removes trailing garbage' do
- io = spy(:io)
- adder = described_class.new(io)
+ context 'authorized_keys_file set' do
+ before do
+ gitlab_shell.remove_all_keys
+ @key = create(:key)
+ gitlab_shell.add_key(@key.shell_id, @key.key)
+ end
- adder.add_key('key-42', "ssh-rsa foo bar\tbaz")
+ it 'does not remove the key' do
+ expect(gitlab_shell).not_to receive(:remove_key).with("key-#{@key.id}")
- expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
+ gitlab_shell.remove_keys_not_found_in_db
+ end
end
+ end
- it 'handles multiple spaces in the key' do
- io = spy(:io)
- adder = described_class.new(io)
+ unless ENV['CI'] # Skip in CI, it takes 1 minute
+ context 'when the first batch can be skipped, but the next batch has keys that are not in the DB' do
+ context 'authorized_keys_file not set' do
+ before do
+ stub_gitlab_shell_setting(authorized_keys_file: nil)
+ gitlab_shell.remove_all_keys
+ 100.times { |i| create(:key) } # first batch is all in the DB
+ gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
+ end
- adder.add_key('key-42', "ssh-rsa foo")
+ it 'removes the keys not in the DB' do
+ expect(gitlab_shell).to receive(:remove_key).with('key-1234')
- expect(io).to have_received(:puts).with("key-42\tssh-rsa foo")
- end
+ gitlab_shell.remove_keys_not_found_in_db
+ end
+ end
- it 'raises an exception if the key contains a tab' do
- expect do
- described_class.new(StringIO.new).add_key('key-42', "ssh-rsa\tfoobar")
- end.to raise_error(Gitlab::Shell::Error)
- end
+ context 'authorized_keys_file set' do
+ before do
+ gitlab_shell.remove_all_keys
+ 100.times { |i| create(:key) } # first batch is all in the DB
+ gitlab_shell.add_key('key-1234', 'ssh-rsa ASDFASDF')
+ end
+
+ it 'removes the keys not in the DB' do
+ expect(gitlab_shell).to receive(:remove_key).with('key-1234')
- it 'raises an exception if the key contains a newline' do
- expect do
- described_class.new(StringIO.new).add_key('key-42', "ssh-rsa foobar\nssh-rsa pawned")
- end.to raise_error(Gitlab::Shell::Error)
+ gitlab_shell.remove_keys_not_found_in_db
+ end
+ end
end
end
end
@@ -393,7 +595,6 @@ describe Gitlab::Shell do
before do
allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(gitlab_shell_path)
- allow(Gitlab.config.gitlab_shell).to receive(:hooks_path).and_return(gitlab_shell_hooks_path)
allow(Gitlab.config.gitlab_shell).to receive(:git_timeout).and_return(800)
end
@@ -411,16 +612,6 @@ describe Gitlab::Shell do
FileUtils.rm_rf(created_path)
end
- it 'creates a repository' do
- expect(gitlab_shell.create_repository(repository_storage, repo_name, repo_name)).to be_truthy
-
- expect(File.stat(created_path).mode & 0o777).to eq(0o770)
-
- hooks_path = File.join(created_path, 'hooks')
- expect(File.lstat(hooks_path)).to be_symlink
- expect(File.realpath(hooks_path)).to eq(gitlab_shell_hooks_path)
- end
-
it 'returns false when the command fails' do
FileUtils.mkdir_p(File.dirname(created_path))
# This file will block the creation of the repo's .git directory. That
@@ -567,12 +758,4 @@ describe Gitlab::Shell do
end
end
end
-
- def find_in_authorized_keys_file(key_id)
- gitlab_shell.batch_read_key_ids do |ids|
- return true if ids.include?(key_id) # rubocop:disable Cop/AvoidReturnFromBlocks
- end
-
- false
- end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
index a138ad7c910..0ff694d409b 100644
--- a/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_injector_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationInjector do
it 'injects into payload the correlation id' do
expect_any_instance_of(described_class).to receive(:call).and_call_original
- Gitlab::CorrelationId.use_id('new-correlation-id') do
+ Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
TestWorker.perform_async(1234)
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
index 94ae4ffa184..8410467ef1f 100644
--- a/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/correlation_logger_spec.rb
@@ -23,7 +23,7 @@ describe Gitlab::SidekiqMiddleware::CorrelationLogger do
expect_any_instance_of(described_class).to receive(:call).and_call_original
expect_any_instance_of(TestWorker).to receive(:perform).with(1234) do
- expect(Gitlab::CorrelationId.current_id).to eq('new-correlation-id')
+ expect(Labkit::Correlation::CorrelationId.current_id).to eq('new-correlation-id')
end
Sidekiq::Client.push(
diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
new file mode 100644
index 00000000000..1a5a38b5d99
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::MemoryKiller do
+ subject { described_class.new }
+ let(:pid) { 999 }
+
+ let(:worker) { double(:worker, class: 'TestWorker') }
+ let(:job) { { 'jid' => 123 } }
+ let(:queue) { 'test_queue' }
+
+ def run
+ thread = subject.call(worker, job, queue) { nil }
+ thread&.join
+ end
+
+ before do
+ allow(subject).to receive(:get_rss).and_return(10.kilobytes)
+ allow(subject).to receive(:pid).and_return(pid)
+ end
+
+ context 'when MAX_RSS is set to 0' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 0)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+
+ context 'when MAX_RSS is exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 5.kilobytes)
+ end
+
+ it 'sends the TSTP, TERM and KILL signals at expected times' do
+ expect(subject).to receive(:sleep).with(15 * 60).ordered
+ expect(Process).to receive(:kill).with('SIGTSTP', pid).ordered
+
+ expect(subject).to receive(:sleep).with(30).ordered
+ expect(Process).to receive(:kill).with('SIGTERM', pid).ordered
+
+ expect(subject).to receive(:sleep).with(10).ordered
+ expect(Process).to receive(:kill).with('SIGKILL', pid).ordered
+
+ run
+ end
+
+ it 'sends TSTP and TERM to the pid, but KILL to the pgroup, when running as process leader' do
+ allow(Process).to receive(:getpgrp) { pid }
+ allow(subject).to receive(:sleep)
+
+ expect(Process).to receive(:kill).with('SIGTSTP', pid).ordered
+ expect(Process).to receive(:kill).with('SIGTERM', pid).ordered
+ expect(Process).to receive(:kill).with('SIGKILL', 0).ordered
+
+ run
+ end
+ end
+
+ context 'when MAX_RSS is not exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 15.kilobytes)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb b/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb
deleted file mode 100644
index 0001795c3f0..00000000000
--- a/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::SidekiqMiddleware::Shutdown do
- subject { described_class.new }
-
- let(:pid) { Process.pid }
- let(:worker) { double(:worker, class: 'TestWorker') }
- let(:job) { { 'jid' => 123 } }
- let(:queue) { 'test_queue' }
- let(:block) { proc { nil } }
-
- def run
- subject.call(worker, job, queue) { block.call }
- described_class.shutdown_thread&.join
- end
-
- def pop_trace
- subject.trace.pop(true)
- end
-
- before do
- allow(subject).to receive(:get_rss).and_return(10.kilobytes)
- described_class.clear_shutdown_thread
- end
-
- context 'when MAX_RSS is set to 0' do
- before do
- stub_const("#{described_class}::MAX_RSS", 0)
- end
-
- it 'does nothing' do
- expect(subject).not_to receive(:sleep)
-
- run
- end
- end
-
- def expect_shutdown_sequence
- expect(pop_trace).to eq([:sleep, 15 * 60])
- expect(pop_trace).to eq([:kill, 'SIGTSTP', pid])
-
- expect(pop_trace).to eq([:sleep, 30])
- expect(pop_trace).to eq([:kill, 'SIGTERM', pid])
-
- expect(pop_trace).to eq([:sleep, 10])
- expect(pop_trace).to eq([:kill, 'SIGKILL', pid])
- end
-
- context 'when MAX_RSS is exceeded' do
- before do
- stub_const("#{described_class}::MAX_RSS", 5.kilobytes)
- end
-
- it 'sends the TSTP, TERM and KILL signals at expected times' do
- run
-
- expect_shutdown_sequence
- end
- end
-
- context 'when MAX_RSS is not exceeded' do
- before do
- stub_const("#{described_class}::MAX_RSS", 15.kilobytes)
- end
-
- it 'does nothing' do
- expect(subject).not_to receive(:sleep)
-
- run
- end
- end
-
- context 'when WantShutdown is raised' do
- let(:block) { proc { raise described_class::WantShutdown } }
-
- it 'starts the shutdown sequence and re-raises the exception' do
- expect { run }.to raise_exception(described_class::WantShutdown)
-
- # We can't expect 'run' to have joined on the shutdown thread, because
- # it hit an exception.
- shutdown_thread = described_class.shutdown_thread
- expect(shutdown_thread).not_to be_nil
- shutdown_thread.join
-
- expect_shutdown_sequence
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_signals_spec.rb b/spec/lib/gitlab/sidekiq_signals_spec.rb
new file mode 100644
index 00000000000..77ecd1840d2
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_signals_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqSignals do
+ describe '.install' do
+ let(:result) { Hash.new { |h, k| h[k] = 0 } }
+ let(:int_handler) { -> (_) { result['INT'] += 1 } }
+ let(:term_handler) { -> (_) { result['TERM'] += 1 } }
+ let(:other_handler) { -> (_) { result['OTHER'] += 1 } }
+ let(:signals) { { 'INT' => int_handler, 'TERM' => term_handler, 'OTHER' => other_handler } }
+
+ context 'not a process group leader' do
+ before do
+ allow(Process).to receive(:getpgrp) { Process.pid * 2 }
+ end
+
+ it 'does nothing' do
+ expect { described_class.install!(signals) }
+ .not_to change { signals }
+ end
+ end
+
+ context 'as a process group leader' do
+ before do
+ allow(Process).to receive(:getpgrp) { Process.pid }
+ end
+
+ it 'installs its own signal handlers for TERM and INT only' do
+ described_class.install!(signals)
+
+ expect(signals['INT']).not_to eq(int_handler)
+ expect(signals['TERM']).not_to eq(term_handler)
+ expect(signals['OTHER']).to eq(other_handler)
+ end
+
+ %w[INT TERM].each do |signal|
+ it "installs a forwarding signal handler for #{signal}" do
+ described_class.install!(signals)
+
+ expect(described_class)
+ .to receive(:trap)
+ .with(signal, 'IGNORE')
+ .and_return(:original_trap)
+ .ordered
+
+ expect(Process)
+ .to receive(:kill)
+ .with(signal, 0)
+ .ordered
+
+ expect(described_class)
+ .to receive(:trap)
+ .with(signal, :original_trap)
+ .ordered
+
+ signals[signal].call(:cli)
+
+ expect(result[signal]).to eq(1)
+ end
+
+ it "raises if sidekiq no longer traps SIG#{signal}" do
+ signals.delete(signal)
+
+ expect { described_class.install!(signals) }
+ .to raise_error(/Sidekiq should have registered/)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
index fe46c67a920..5f0a7e925ca 100644
--- a/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
+++ b/spec/lib/gitlab/template/gitlab_ci_yml_template_spec.rb
@@ -15,6 +15,13 @@ describe Gitlab::Template::GitlabCiYmlTemplate do
expect(all).to include('Docker')
expect(all).to include('Ruby')
end
+
+ it 'ensure that the template name is used exactly once' do
+ all = subject.all.group_by(&:name)
+ duplicates = all.select { |_, templates| templates.length > 1 }
+
+ expect(duplicates).to be_empty
+ end
end
describe '.find' do
diff --git a/spec/lib/gitlab/tracing/factory_spec.rb b/spec/lib/gitlab/tracing/factory_spec.rb
deleted file mode 100644
index 945490f0988..00000000000
--- a/spec/lib/gitlab/tracing/factory_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::Factory do
- describe '.create_tracer' do
- let(:service_name) { 'rspec' }
-
- context "when tracing is not configured" do
- it 'ignores null connection strings' do
- expect(described_class.create_tracer(service_name, nil)).to be_nil
- end
-
- it 'ignores empty connection strings' do
- expect(described_class.create_tracer(service_name, '')).to be_nil
- end
-
- it 'ignores unknown implementations' do
- expect(described_class.create_tracer(service_name, 'opentracing://invalid_driver')).to be_nil
- end
-
- it 'ignores invalid connection strings' do
- expect(described_class.create_tracer(service_name, 'open?tracing')).to be_nil
- end
- end
-
- context "when tracing is configured with jaeger" do
- let(:mock_tracer) { double('tracer') }
-
- it 'processes default connections' do
- expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, {}).and_return(mock_tracer)
-
- expect(described_class.create_tracer(service_name, 'opentracing://jaeger')).to be(mock_tracer)
- end
-
- it 'processes connections with parameters' do
- expect(Gitlab::Tracing::JaegerFactory).to receive(:create_tracer).with(service_name, { a: '1', b: '2', c: '3' }).and_return(mock_tracer)
-
- expect(described_class.create_tracer(service_name, 'opentracing://jaeger?a=1&b=2&c=3')).to be(mock_tracer)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb b/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb
deleted file mode 100644
index 7f5aecb7baa..00000000000
--- a/spec/lib/gitlab/tracing/grpc_interceptor_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::GRPCInterceptor do
- subject { described_class.instance }
-
- shared_examples_for "a grpc interceptor method" do
- let(:custom_error) { Class.new(StandardError) }
-
- it 'yields' do
- expect { |b| method.call(kwargs, &b) }.to yield_control
- end
-
- it 'propagates exceptions' do
- expect { method.call(kwargs) { raise custom_error } }.to raise_error(custom_error)
- end
- end
-
- describe '#request_response' do
- let(:method) { subject.method(:request_response) }
- let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } }
-
- it_behaves_like 'a grpc interceptor method'
- end
-
- describe '#client_streamer' do
- let(:method) { subject.method(:client_streamer) }
- let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } }
-
- it_behaves_like 'a grpc interceptor method'
- end
-
- describe '#server_streamer' do
- let(:method) { subject.method(:server_streamer) }
- let(:kwargs) { { request: {}, call: {}, method: 'grc_method', metadata: {} } }
-
- it_behaves_like 'a grpc interceptor method'
- end
-
- describe '#bidi_streamer' do
- let(:method) { subject.method(:bidi_streamer) }
- let(:kwargs) { { requests: [], call: {}, method: 'grc_method', metadata: {} } }
-
- it_behaves_like 'a grpc interceptor method'
- end
-end
diff --git a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb b/spec/lib/gitlab/tracing/jaeger_factory_spec.rb
deleted file mode 100644
index 3d6a007cfd9..00000000000
--- a/spec/lib/gitlab/tracing/jaeger_factory_spec.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::JaegerFactory do
- describe '.create_tracer' do
- let(:service_name) { 'rspec' }
-
- shared_examples_for 'a jaeger tracer' do
- it 'responds to active_span methods' do
- expect(tracer).to respond_to(:active_span)
- end
-
- it 'yields control' do
- expect { |b| tracer.start_active_span('operation_name', &b) }.to yield_control
- end
- end
-
- context 'processes default connections' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, {}) }
- end
- end
-
- context 'handles debug options' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { debug: "1" }) }
- end
- end
-
- context 'handles const sampler' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { sampler: "const", sampler_param: "1" }) }
- end
- end
-
- context 'handles probabilistic sampler' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { sampler: "probabilistic", sampler_param: "0.5" }) }
- end
- end
-
- context 'handles http_endpoint configurations' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { http_endpoint: "http://localhost:1234" }) }
- end
- end
-
- context 'handles udp_endpoint configurations' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { udp_endpoint: "localhost:4321" }) }
- end
- end
-
- context 'ignores invalid parameters' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { invalid: "true" }) }
- end
- end
-
- context 'accepts the debug parameter when strict_parser is set' do
- it_behaves_like 'a jaeger tracer' do
- let(:tracer) { described_class.create_tracer(service_name, { debug: "1", strict_parsing: "1" }) }
- end
- end
-
- it 'rejects invalid parameters when strict_parser is set' do
- expect { described_class.create_tracer(service_name, { invalid: "true", strict_parsing: "1" }) }.to raise_error(StandardError)
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/rack_middleware_spec.rb b/spec/lib/gitlab/tracing/rack_middleware_spec.rb
deleted file mode 100644
index 13d4d8a89f7..00000000000
--- a/spec/lib/gitlab/tracing/rack_middleware_spec.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::Tracing::RackMiddleware do
- using RSpec::Parameterized::TableSyntax
-
- describe '#call' do
- context 'for normal middleware flow' do
- let(:fake_app) { -> (env) { fake_app_response } }
- subject { described_class.new(fake_app) }
- let(:request) { }
-
- context 'for 200 responses' do
- let(:fake_app_response) { [200, { 'Content-Type': 'text/plain' }, ['OK']] }
-
- it 'delegates correctly' do
- expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response)
- end
- end
-
- context 'for 500 responses' do
- let(:fake_app_response) { [500, { 'Content-Type': 'text/plain' }, ['Error']] }
-
- it 'delegates correctly' do
- expect(subject.call(Rack::MockRequest.env_for("/"))).to eq(fake_app_response)
- end
- end
- end
-
- context 'when an application is raising an exception' do
- let(:custom_error) { Class.new(StandardError) }
- let(:fake_app) { ->(env) { raise custom_error } }
-
- subject { described_class.new(fake_app) }
-
- it 'delegates propagates exceptions correctly' do
- expect { subject.call(Rack::MockRequest.env_for("/")) }.to raise_error(custom_error)
- end
- end
- end
-
- describe '.build_sanitized_url_from_env' do
- def env_for_url(url)
- env = Rack::MockRequest.env_for(input_url)
- env['action_dispatch.parameter_filter'] = [/token/]
-
- env
- end
-
- where(:input_url, :output_url) do
- '/gitlab-org/gitlab-ce' | 'http://example.org/gitlab-org/gitlab-ce'
- '/gitlab-org/gitlab-ce?safe=1' | 'http://example.org/gitlab-org/gitlab-ce?safe=1'
- '/gitlab-org/gitlab-ce?private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?private_token=%5BFILTERED%5D'
- '/gitlab-org/gitlab-ce?mixed=1&private_token=secret' | 'http://example.org/gitlab-org/gitlab-ce?mixed=1&private_token=%5BFILTERED%5D'
- end
-
- with_them do
- it { expect(described_class.build_sanitized_url_from_env(env_for_url(input_url))).to eq(output_url) }
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
deleted file mode 100644
index c9d1a06b3e6..00000000000
--- a/spec/lib/gitlab/tracing/rails/action_view_subscriber_spec.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-
-describe Gitlab::Tracing::Rails::ActionViewSubscriber do
- using RSpec::Parameterized::TableSyntax
-
- shared_examples 'an actionview notification' do
- it 'should notify the tracer when the hash contains null values' do
- expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
-
- subject.public_send(notify_method, start, finish, payload)
- end
-
- it 'should notify the tracer when the payload is missing values' do
- expect(subject).to receive(:postnotify_span).with(notification_name, start, finish, tags: expected_tags, exception: exception)
-
- subject.public_send(notify_method, start, finish, payload.compact)
- end
-
- it 'should not throw exceptions when with the default tracer' do
- expect { subject.public_send(notify_method, start, finish, payload) }.not_to raise_error
- end
- end
-
- describe '.instrument' do
- it 'is unsubscribeable' do
- unsubscribe = described_class.instrument
-
- expect(unsubscribe).not_to be_nil
- expect { unsubscribe.call }.not_to raise_error
- end
- end
-
- describe '#notify_render_template' do
- subject { described_class.new }
- let(:start) { Time.now }
- let(:finish) { Time.now }
- let(:notification_name) { 'render_template' }
- let(:notify_method) { :notify_render_template }
-
- where(:identifier, :layout, :exception) do
- nil | nil | nil
- "" | nil | nil
- "show.haml" | nil | nil
- nil | "" | nil
- nil | "layout.haml" | nil
- nil | nil | StandardError.new
- end
-
- with_them do
- let(:payload) do
- {
- exception: exception,
- identifier: identifier,
- layout: layout
- }
- end
-
- let(:expected_tags) do
- {
- 'component' => 'ActionView',
- 'template.id' => identifier,
- 'template.layout' => layout
- }
- end
-
- it_behaves_like 'an actionview notification'
- end
- end
-
- describe '#notify_render_collection' do
- subject { described_class.new }
- let(:start) { Time.now }
- let(:finish) { Time.now }
- let(:notification_name) { 'render_collection' }
- let(:notify_method) { :notify_render_collection }
-
- where(
- :identifier, :count, :expected_count, :cache_hits, :expected_cache_hits, :exception) do
- nil | nil | 0 | nil | 0 | nil
- "" | nil | 0 | nil | 0 | nil
- "show.haml" | nil | 0 | nil | 0 | nil
- nil | 0 | 0 | nil | 0 | nil
- nil | 1 | 1 | nil | 0 | nil
- nil | nil | 0 | 0 | 0 | nil
- nil | nil | 0 | 1 | 1 | nil
- nil | nil | 0 | nil | 0 | StandardError.new
- end
-
- with_them do
- let(:payload) do
- {
- exception: exception,
- identifier: identifier,
- count: count,
- cache_hits: cache_hits
- }
- end
-
- let(:expected_tags) do
- {
- 'component' => 'ActionView',
- 'template.id' => identifier,
- 'template.count' => expected_count,
- 'template.cache.hits' => expected_cache_hits
- }
- end
-
- it_behaves_like 'an actionview notification'
- end
- end
-
- describe '#notify_render_partial' do
- subject { described_class.new }
- let(:start) { Time.now }
- let(:finish) { Time.now }
- let(:notification_name) { 'render_partial' }
- let(:notify_method) { :notify_render_partial }
-
- where(:identifier, :exception) do
- nil | nil
- "" | nil
- "show.haml" | nil
- nil | StandardError.new
- end
-
- with_them do
- let(:payload) do
- {
- exception: exception,
- identifier: identifier
- }
- end
-
- let(:expected_tags) do
- {
- 'component' => 'ActionView',
- 'template.id' => identifier
- }
- end
-
- it_behaves_like 'an actionview notification'
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb b/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
deleted file mode 100644
index 3d066843148..00000000000
--- a/spec/lib/gitlab/tracing/rails/active_record_subscriber_spec.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require 'rspec-parameterized'
-
-describe Gitlab::Tracing::Rails::ActiveRecordSubscriber do
- using RSpec::Parameterized::TableSyntax
-
- describe '.instrument' do
- it 'is unsubscribeable' do
- unsubscribe = described_class.instrument
-
- expect(unsubscribe).not_to be_nil
- expect { unsubscribe.call }.not_to raise_error
- end
- end
-
- describe '#notify' do
- subject { described_class.new }
- let(:start) { Time.now }
- let(:finish) { Time.now }
-
- where(:name, :operation_name, :exception, :connection_id, :cached, :cached_response, :sql) do
- nil | "active_record:sqlquery" | nil | nil | nil | false | nil
- "" | "active_record:sqlquery" | nil | nil | nil | false | nil
- "User Load" | "active_record:User Load" | nil | nil | nil | false | nil
- "Repo Load" | "active_record:Repo Load" | StandardError.new | nil | nil | false | nil
- nil | "active_record:sqlquery" | nil | 123 | nil | false | nil
- nil | "active_record:sqlquery" | nil | nil | false | false | nil
- nil | "active_record:sqlquery" | nil | nil | true | true | nil
- nil | "active_record:sqlquery" | nil | nil | true | true | "SELECT * FROM users"
- end
-
- with_them do
- def payload
- {
- name: name,
- exception: exception,
- connection_id: connection_id,
- cached: cached,
- sql: sql
- }
- end
-
- def expected_tags
- {
- "component" => "ActiveRecord",
- "span.kind" => "client",
- "db.type" => "sql",
- "db.connection_id" => connection_id,
- "db.cached" => cached_response,
- "db.statement" => sql
- }
- end
-
- it 'should notify the tracer when the hash contains null values' do
- expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
-
- subject.notify(start, finish, payload)
- end
-
- it 'should notify the tracer when the payload is missing values' do
- expect(subject).to receive(:postnotify_span).with(operation_name, start, finish, tags: expected_tags, exception: exception)
-
- subject.notify(start, finish, payload.compact)
- end
-
- it 'should not throw exceptions when with the default tracer' do
- expect { subject.notify(start, finish, payload) }.not_to raise_error
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb b/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb
deleted file mode 100644
index 3755860b5ba..00000000000
--- a/spec/lib/gitlab/tracing/sidekiq/client_middleware_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::Sidekiq::ClientMiddleware do
- describe '#call' do
- let(:worker_class) { 'test_worker_class' }
- let(:job) do
- {
- 'class' => "jobclass",
- 'queue' => "jobqueue",
- 'retry' => 0,
- 'args' => %w{1 2 3}
- }
- end
- let(:queue) { 'test_queue' }
- let(:redis_pool) { double("redis_pool") }
- let(:custom_error) { Class.new(StandardError) }
- let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) }
-
- subject { described_class.new }
-
- it 'yields' do
- expect(subject).to receive(:in_tracing_span).with(
- operation_name: "sidekiq:jobclass",
- tags: {
- "component" => "sidekiq",
- "span.kind" => "client",
- "sidekiq.queue" => "jobqueue",
- "sidekiq.jid" => nil,
- "sidekiq.retry" => "0",
- "sidekiq.args" => "1, 2, 3"
- }
- ).and_yield(span)
-
- expect { |b| subject.call(worker_class, job, queue, redis_pool, &b) }.to yield_control
- end
-
- it 'propagates exceptions' do
- expect { subject.call(worker_class, job, queue, redis_pool) { raise custom_error } }.to raise_error(custom_error)
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb b/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb
deleted file mode 100644
index c3087de785a..00000000000
--- a/spec/lib/gitlab/tracing/sidekiq/server_middleware_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-
-describe Gitlab::Tracing::Sidekiq::ServerMiddleware do
- describe '#call' do
- let(:worker_class) { 'test_worker_class' }
- let(:job) do
- {
- 'class' => "jobclass",
- 'queue' => "jobqueue",
- 'retry' => 0,
- 'args' => %w{1 2 3}
- }
- end
- let(:queue) { 'test_queue' }
- let(:custom_error) { Class.new(StandardError) }
- let(:span) { OpenTracing.start_span('test', ignore_active_scope: true) }
- subject { described_class.new }
-
- it 'yields' do
- expect(subject).to receive(:in_tracing_span).with(
- hash_including(
- operation_name: "sidekiq:jobclass",
- tags: {
- "component" => "sidekiq",
- "span.kind" => "server",
- "sidekiq.queue" => "jobqueue",
- "sidekiq.jid" => nil,
- "sidekiq.retry" => "0",
- "sidekiq.args" => "1, 2, 3"
- }
- )
- ).and_yield(span)
-
- expect { |b| subject.call(worker_class, job, queue, &b) }.to yield_control
- end
-
- it 'propagates exceptions' do
- expect { subject.call(worker_class, job, queue) { raise custom_error } }.to raise_error(custom_error)
- end
- end
-end
diff --git a/spec/lib/gitlab/tracing_spec.rb b/spec/lib/gitlab/tracing_spec.rb
index 566b5050e47..db75ce2a998 100644
--- a/spec/lib/gitlab/tracing_spec.rb
+++ b/spec/lib/gitlab/tracing_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Tracing do
end
with_them do
- it 'should return the correct state for .enabled?' do
+ it 'returns the correct state for .enabled?' do
expect(described_class).to receive(:connection_string).and_return(connection_string)
expect(described_class.enabled?).to eq(enabled_state)
@@ -33,7 +33,7 @@ describe Gitlab::Tracing do
end
with_them do
- it 'should return the correct state for .tracing_url_enabled?' do
+ it 'returns the correct state for .tracing_url_enabled?' do
expect(described_class).to receive(:enabled?).and_return(enabled?)
allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template)
@@ -56,7 +56,7 @@ describe Gitlab::Tracing do
end
with_them do
- it 'should return the correct state for .tracing_url' do
+ it 'returns the correct state for .tracing_url' do
expect(described_class).to receive(:tracing_url_enabled?).and_return(tracing_url_enabled?)
allow(described_class).to receive(:tracing_url_template).and_return(tracing_url_template)
allow(Gitlab::CorrelationId).to receive(:current_id).and_return(correlation_id)
diff --git a/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb
new file mode 100644
index 00000000000..f1882e03581
--- /dev/null
+++ b/spec/lib/gitlab/untrusted_regexp/ruby_syntax_spec.rb
@@ -0,0 +1,118 @@
+require 'fast_spec_helper'
+require 'support/shared_examples/malicious_regexp_shared_examples'
+require 'support/helpers/stub_feature_flags'
+
+describe Gitlab::UntrustedRegexp::RubySyntax do
+ describe '.matches_syntax?' do
+ it 'returns true if regexp is valid' do
+ expect(described_class.matches_syntax?('/some .* thing/'))
+ .to be true
+ end
+
+ it 'returns true if regexp is invalid, but resembles regexp' do
+ expect(described_class.matches_syntax?('/some ( thing/'))
+ .to be true
+ end
+ end
+
+ describe '.valid?' do
+ it 'returns true if regexp is valid' do
+ expect(described_class.valid?('/some .* thing/'))
+ .to be true
+ end
+
+ it 'returns false if regexp is invalid' do
+ expect(described_class.valid?('/some ( thing/'))
+ .to be false
+ end
+ end
+
+ describe '.fabricate' do
+ context 'when regexp is valid' do
+ it 'fabricates regexp without flags' do
+ expect(described_class.fabricate('/some .* thing/')).not_to be_nil
+ end
+ end
+
+ context 'when regexp is empty' do
+ it 'fabricates regexp correctly' do
+ expect(described_class.fabricate('//')).not_to be_nil
+ end
+ end
+
+ context 'when regexp is a raw pattern' do
+ it 'returns error' do
+ expect(described_class.fabricate('some .* thing')).to be_nil
+ end
+ end
+ end
+
+ describe '.fabricate!' do
+ context 'safe regexp is used' do
+ context 'when regexp is using /regexp/ scheme with flags' do
+ it 'fabricates regexp with a single flag' do
+ regexp = described_class.fabricate!('/something/i')
+
+ expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?i)something')
+ expect(regexp.scan('SOMETHING')).to be_one
+ end
+
+ it 'fabricates regexp with multiple flags' do
+ regexp = described_class.fabricate!('/something/im')
+
+ expect(regexp).to eq Gitlab::UntrustedRegexp.new('(?im)something')
+ end
+
+ it 'fabricates regexp without flags' do
+ regexp = described_class.fabricate!('/something/')
+
+ expect(regexp).to eq Gitlab::UntrustedRegexp.new('something')
+ end
+ end
+ end
+
+ context 'when unsafe regexp is used' do
+ include StubFeatureFlags
+
+ before do
+ stub_feature_flags(allow_unsafe_ruby_regexp: true)
+
+ allow(Gitlab::UntrustedRegexp).to receive(:new).and_raise(RegexpError)
+ end
+
+ context 'when no fallback is enabled' do
+ it 'raises an exception' do
+ expect { described_class.fabricate!('/something/') }
+ .to raise_error(RegexpError)
+ end
+ end
+
+ context 'when fallback is used' do
+ it 'fabricates regexp with a single flag' do
+ regexp = described_class.fabricate!('/something/i', fallback: true)
+
+ expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE)
+ end
+
+ it 'fabricates regexp with multiple flags' do
+ regexp = described_class.fabricate!('/something/im', fallback: true)
+
+ expect(regexp).to eq Regexp.new('something', Regexp::IGNORECASE | Regexp::MULTILINE)
+ end
+
+ it 'fabricates regexp without flags' do
+ regexp = described_class.fabricate!('/something/', fallback: true)
+
+ expect(regexp).to eq Regexp.new('something')
+ end
+ end
+ end
+
+ context 'when regexp is a raw pattern' do
+ it 'raises an error' do
+ expect { described_class.fabricate!('some .* thing') }
+ .to raise_error(RegexpError)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb
index 0a6ac0aa294..9d483f13a5e 100644
--- a/spec/lib/gitlab/untrusted_regexp_spec.rb
+++ b/spec/lib/gitlab/untrusted_regexp_spec.rb
@@ -2,48 +2,6 @@ require 'fast_spec_helper'
require 'support/shared_examples/malicious_regexp_shared_examples'
describe Gitlab::UntrustedRegexp do
- describe '.valid?' do
- it 'returns true if regexp is valid' do
- expect(described_class.valid?('/some ( thing/'))
- .to be false
- end
-
- it 'returns true if regexp is invalid' do
- expect(described_class.valid?('/some .* thing/'))
- .to be true
- end
- end
-
- describe '.fabricate' do
- context 'when regexp is using /regexp/ scheme with flags' do
- it 'fabricates regexp with a single flag' do
- regexp = described_class.fabricate('/something/i')
-
- expect(regexp).to eq described_class.new('(?i)something')
- expect(regexp.scan('SOMETHING')).to be_one
- end
-
- it 'fabricates regexp with multiple flags' do
- regexp = described_class.fabricate('/something/im')
-
- expect(regexp).to eq described_class.new('(?im)something')
- end
-
- it 'fabricates regexp without flags' do
- regexp = described_class.fabricate('/something/')
-
- expect(regexp).to eq described_class.new('something')
- end
- end
-
- context 'when regexp is a raw pattern' do
- it 'raises an error' do
- expect { described_class.fabricate('some .* thing') }
- .to raise_error(RegexpError)
- end
- end
- end
-
describe '#initialize' do
subject { described_class.new(pattern) }
@@ -92,11 +50,41 @@ describe Gitlab::UntrustedRegexp do
end
end
+ describe '#match?' do
+ subject { described_class.new(regexp).match?(text) }
+
+ context 'malicious regexp' do
+ let(:text) { malicious_text }
+ let(:regexp) { malicious_regexp_re2 }
+
+ include_examples 'malicious regexp'
+ end
+
+ context 'matching regexp' do
+ let(:regexp) { 'foo' }
+ let(:text) { 'foo' }
+
+ it 'returns an array of nil matches' do
+ is_expected.to eq(true)
+ end
+ end
+
+ context 'non-matching regexp' do
+ let(:regexp) { 'boo' }
+ let(:text) { 'foo' }
+
+ it 'returns an array of nil matches' do
+ is_expected.to eq(false)
+ end
+ end
+ end
+
describe '#scan' do
subject { described_class.new(regexp).scan(text) }
+
context 'malicious regexp' do
let(:text) { malicious_text }
- let(:regexp) { malicious_regexp }
+ let(:regexp) { malicious_regexp_re2 }
include_examples 'malicious regexp'
end
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 62970bd8cb6..253366e0789 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -2,6 +2,87 @@
require 'spec_helper'
describe Gitlab::UrlBlocker do
+ describe '#validate!' do
+ context 'when URI is nil' do
+ let(:import_url) { nil }
+
+ it 'returns no URI and hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to be(nil)
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'when URI is internal' do
+ let(:import_url) { 'http://localhost' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('http://[::1]'))
+ expect(hostname).to eq('localhost')
+ end
+ end
+
+ context 'when the URL hostname is a domain' do
+ let(:import_url) { 'https://example.org' }
+
+ it 'returns URI and hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to eq('example.org')
+ end
+ end
+
+ context 'when the URL hostname is an IP address' do
+ let(:import_url) { 'https://93.184.216.34' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'disabled DNS rebinding protection' do
+ context 'when URI is internal' do
+ let(:import_url) { 'http://localhost' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('http://localhost'))
+ expect(hostname).to be(nil)
+ end
+ end
+
+ context 'when the URL hostname is a domain' do
+ let(:import_url) { 'https://example.org' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('https://example.org'))
+ expect(hostname).to eq(nil)
+ end
+ end
+
+ context 'when the URL hostname is an IP address' do
+ let(:import_url) { 'https://93.184.216.34' }
+
+ it 'returns URI and no hostname' do
+ uri, hostname = described_class.validate!(import_url, dns_rebind_protection: false)
+
+ expect(uri).to eq(Addressable::URI.parse('https://93.184.216.34'))
+ expect(hostname).to be(nil)
+ end
+ end
+ end
+ end
+
describe '#blocked_url?' do
let(:ports) { Project::VALID_IMPORT_PORTS }
@@ -23,10 +104,10 @@ describe Gitlab::UrlBlocker do
expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git', ports: ports)).to be true
end
- it 'returns true for bad protocol' do
- expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['https'])).to be false
+ it 'returns true for bad scheme' do
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', schemes: ['https'])).to be false
expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
- expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', protocols: ['http'])).to be true
+ expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git', schemes: ['http'])).to be true
end
it 'returns true for bad protocol on configured web/SSH host and ports' do
@@ -208,7 +289,7 @@ describe Gitlab::UrlBlocker do
end
def stub_domain_resolv(domain, ip)
- address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false)
+ address = double(ip_address: ip, ipv4_private?: true, ipv6_link_local?: false, ipv4_loopback?: false, ipv6_loopback?: false, ipv4?: false)
allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([address])
allow(address).to receive(:ipv6_v4mapped?).and_return(false)
end
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 9f495a5d50b..bbcb92608d8 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::UrlBuilder do
url = described_class.build(milestone)
- expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/milestones/#{milestone.iid}"
+ expect(url).to eq "#{Settings.gitlab['url']}/#{milestone.project.full_path}/-/milestones/#{milestone.iid}"
end
end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 6e98a999766..7242255d535 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -115,6 +115,40 @@ describe Gitlab::UrlSanitizer do
end
end
+ describe '#user' do
+ context 'credentials in hash' do
+ it 'overrides URL-provided user' do
+ sanitizer = described_class.new('http://a:b@example.com', credentials: { user: 'c', password: 'd' })
+
+ expect(sanitizer.user).to eq('c')
+ end
+ end
+
+ context 'credentials in URL' do
+ where(:url, :user) do
+ 'http://foo:bar@example.com' | 'foo'
+ 'http://foo:bar:baz@example.com' | 'foo'
+ 'http://:bar@example.com' | nil
+ 'http://foo:@example.com' | 'foo'
+ 'http://foo@example.com' | 'foo'
+ 'http://:@example.com' | nil
+ 'http://@example.com' | nil
+ 'http://example.com' | nil
+
+ # Other invalid URLs
+ nil | nil
+ '' | nil
+ 'no' | nil
+ end
+
+ with_them do
+ subject { described_class.new(url).user }
+
+ it { is_expected.to eq(user) }
+ end
+ end
+ end
+
describe '#full_url' do
context 'credentials in hash' do
where(:credentials, :userinfo) do
@@ -161,7 +195,7 @@ describe Gitlab::UrlSanitizer do
end
context 'when credentials contains special chars' do
- it 'should parse the URL without errors' do
+ it 'parses the URL without errors' do
url_sanitizer = described_class.new("https://foo:b?r@github.com/me/project.git")
expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git")
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index d3eae80cc56..e44463dd767 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -13,6 +13,8 @@ describe Gitlab::UsageData do
create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
create(:service, project: projects[1], type: 'SlackService', active: true)
create(:service, project: projects[2], type: 'SlackService', active: true)
+ create(:project_error_tracking_setting, project: projects[0])
+ create(:project_error_tracking_setting, project: projects[1], enabled: false)
gcp_cluster = create(:cluster, :provided_by_gcp)
create(:cluster, :provided_by_user)
@@ -33,7 +35,7 @@ describe Gitlab::UsageData do
subject { described_class.data }
it "gathers usage data" do
- expect(subject.keys).to match_array(%i(
+ expect(subject.keys).to include(*%i(
active_user_count
counts
recorded_at
@@ -55,16 +57,13 @@ describe Gitlab::UsageData do
database
avg_cycle_analytics
web_ide_commits
+ influxdb_metrics_enabled
+ prometheus_metrics_enabled
))
end
it "gathers usage counts" do
- count_data = subject[:counts]
-
- expect(count_data[:boards]).to eq(1)
- expect(count_data[:projects]).to eq(3)
-
- expect(count_data.keys).to match_array(%i(
+ expected_keys = %i(
assignee_lists
boards
ci_builds
@@ -79,6 +78,8 @@ describe Gitlab::UsageData do
auto_devops_disabled
deploy_keys
deployments
+ successful_deployments
+ failed_deployments
environments
clusters
clusters_enabled
@@ -106,6 +107,7 @@ describe Gitlab::UsageData do
milestone_lists
milestones
notes
+ pool_repositories
projects
projects_imported_from_github
projects_jira_active
@@ -115,6 +117,7 @@ describe Gitlab::UsageData do
projects_slack_slash_active
projects_prometheus_active
projects_with_repositories_enabled
+ projects_with_error_tracking_enabled
pages_domains
protected_branches
releases
@@ -125,7 +128,14 @@ describe Gitlab::UsageData do
uploads
web_hooks
user_preferences
- ))
+ )
+
+ count_data = subject[:counts]
+
+ expect(count_data[:boards]).to eq(1)
+ expect(count_data[:projects]).to eq(3)
+ expect(count_data.keys).to include(*expected_keys)
+ expect(expected_keys - count_data.keys).to be_empty
end
it 'does not gather user preferences usage data when the feature is disabled' do
@@ -144,6 +154,7 @@ describe Gitlab::UsageData do
expect(count_data[:projects_slack_notifications_active]).to eq(2)
expect(count_data[:projects_slack_slash_active]).to eq(1)
expect(count_data[:projects_with_repositories_enabled]).to eq(2)
+ expect(count_data[:projects_with_error_tracking_enabled]).to eq(1)
expect(count_data[:clusters_enabled]).to eq(7)
expect(count_data[:project_clusters_enabled]).to eq(6)
@@ -203,7 +214,7 @@ describe Gitlab::UsageData do
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::INSTALLATION_TYPE)
+ expect(subject[:installation_type]).to eq('gitlab-development-kit')
expect(subject[:active_user_count]).to eq(User.active.count)
expect(subject[:recorded_at]).to be_a(Time)
end
diff --git a/spec/lib/gitlab/user_extractor_spec.rb b/spec/lib/gitlab/user_extractor_spec.rb
index fcc05ab3a0c..b86ec5445b8 100644
--- a/spec/lib/gitlab/user_extractor_spec.rb
+++ b/spec/lib/gitlab/user_extractor_spec.rb
@@ -38,6 +38,18 @@ describe Gitlab::UserExtractor do
expect(extractor.users).to include(user)
end
+
+ context 'input as array of strings' do
+ it 'is treated as one string' do
+ extractor = described_class.new(text.lines)
+
+ user_1 = create(:user, username: "USER-1")
+ user_4 = create(:user, username: "USER-4")
+ user_email = create(:user, email: 'user@gitlab.org')
+
+ expect(extractor.users).to contain_exactly(user_1, user_4, user_email)
+ end
+ end
end
describe '#matches' do
@@ -48,6 +60,14 @@ describe Gitlab::UserExtractor do
it 'includes all mentioned usernames' do
expect(extractor.matches[:usernames]).to contain_exactly('user-1', 'user-2', 'user-4')
end
+
+ context 'input has no matching e-mail or usernames' do
+ it 'returns an empty list of users' do
+ extractor = described_class.new('My test')
+
+ expect(extractor.users).to be_empty
+ end
+ end
end
describe '#references' do
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 8f5029b3565..4645339f439 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -213,4 +213,22 @@ describe Gitlab::Utils do
expect(subject[:variables].first[:key]).to eq('VAR1')
end
end
+
+ describe '.try_megabytes_to_bytes' do
+ context 'when the size can be converted to megabytes' do
+ it 'returns the size in megabytes' do
+ size = described_class.try_megabytes_to_bytes(1)
+
+ expect(size).to eq(1.megabytes)
+ end
+ end
+
+ context 'when the size can not be converted to megabytes' do
+ it 'returns the input size' do
+ size = described_class.try_megabytes_to_bytes('foo')
+
+ expect(size).to eq('foo')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
index 2c1146ceff5..0a170a157fe 100644
--- a/spec/lib/gitlab/visibility_level_spec.rb
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -85,4 +85,12 @@ describe Gitlab::VisibilityLevel do
.to eq(described_class::PRIVATE)
end
end
+
+ describe '.valid_level?' do
+ it 'returns true when visibility is valid' do
+ expect(described_class.valid_level?(described_class::PRIVATE)).to be_truthy
+ expect(described_class.valid_level?(described_class::INTERNAL)).to be_truthy
+ expect(described_class.valid_level?(described_class::PUBLIC)).to be_truthy
+ end
+ end
end
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 7213eee5675..f8332757fcd 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -16,40 +16,80 @@ describe Gitlab::Workhorse do
let(:ref) { 'master' }
let(:format) { 'zip' }
let(:storage_path) { Gitlab.config.gitlab.repository_downloads_path }
- let(:base_params) { repository.archive_metadata(ref, storage_path, format, append_sha: nil) }
- let(:gitaly_params) do
- base_params.merge(
- 'GitalyServer' => {
- 'address' => Gitlab::GitalyClient.address(project.repository_storage),
- 'token' => Gitlab::GitalyClient.token(project.repository_storage)
- },
- 'GitalyRepository' => repository.gitaly_repository.to_h.deep_stringify_keys
- )
- end
+ let(:path) { 'some/path' if Feature.enabled?(:git_archive_path, default_enabled: true) }
+ let(:metadata) { repository.archive_metadata(ref, storage_path, format, append_sha: nil, path: path) }
let(:cache_disabled) { false }
subject do
- described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil)
+ described_class.send_git_archive(repository, ref: ref, format: format, append_sha: nil, path: path)
end
before do
allow(described_class).to receive(:git_archive_cache_disabled?).and_return(cache_disabled)
end
- it 'sets the header correctly' do
- key, command, params = decode_workhorse_header(subject)
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(git_archive_path: false)
+ end
- expect(key).to eq('Gitlab-Workhorse-Send-Data')
- expect(command).to eq('git-archive')
- expect(params).to include(gitaly_params)
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expected_params = metadata.merge(
+ 'GitalyRepository' => repository.gitaly_repository.to_h,
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ }
+ )
+
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to eq(expected_params.deep_stringify_keys)
+ end
+
+ context 'when archive caching is disabled' do
+ let(:cache_disabled) { true }
+
+ it 'tells workhorse not to use the cache' do
+ _, _, params = decode_workhorse_header(subject)
+ expect(params).to include({ 'DisableCache' => true })
+ end
+ end
end
- context 'when archive caching is disabled' do
- let(:cache_disabled) { true }
+ context 'feature flag enabled' do
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq('Gitlab-Workhorse-Send-Data')
+ expect(command).to eq('git-archive')
+ expect(params).to eq({
+ 'GitalyServer' => {
+ address: Gitlab::GitalyClient.address(project.repository_storage),
+ token: Gitlab::GitalyClient.token(project.repository_storage)
+ },
+ 'ArchivePath' => metadata['ArchivePath'],
+ 'GetArchiveRequest' => Base64.encode64(
+ Gitaly::GetArchiveRequest.new(
+ repository: repository.gitaly_repository,
+ commit_id: metadata['CommitId'],
+ prefix: metadata['ArchivePrefix'],
+ format: Gitaly::GetArchiveRequest::Format::ZIP,
+ path: path
+ ).to_proto
+ )
+ }.deep_stringify_keys)
+ end
- it 'tells workhorse not to use the cache' do
- _, _, params = decode_workhorse_header(subject)
- expect(params).to include({ 'DisableCache' => true })
+ context 'when archive caching is disabled' do
+ let(:cache_disabled) { true }
+
+ it 'tells workhorse not to use the cache' do
+ _, _, params = decode_workhorse_header(subject)
+ expect(params).to include({ 'DisableCache' => true })
+ end
end
end
@@ -87,7 +127,7 @@ describe Gitlab::Workhorse do
end
end
- describe '.terminal_websocket' do
+ describe '.channel_websocket' do
def terminal(ca_pem: nil)
out = {
subprotocols: ['foo'],
@@ -101,25 +141,25 @@ describe Gitlab::Workhorse do
def workhorse(ca_pem: nil)
out = {
- 'Terminal' => {
+ 'Channel' => {
'Subprotocols' => ['foo'],
'Url' => 'wss://example.com/terminal.ws',
'Header' => { 'Authorization' => ['Token x'] },
'MaxSessionTime' => 600
}
}
- out['Terminal']['CAPem'] = ca_pem if ca_pem
+ out['Channel']['CAPem'] = ca_pem if ca_pem
out
end
context 'without ca_pem' do
- subject { described_class.terminal_websocket(terminal) }
+ subject { described_class.channel_websocket(terminal) }
it { is_expected.to eq(workhorse) }
end
context 'with ca_pem' do
- subject { described_class.terminal_websocket(terminal(ca_pem: "foo")) }
+ subject { described_class.channel_websocket(terminal(ca_pem: "foo")) }
it { is_expected.to eq(workhorse(ca_pem: "foo")) }
end
@@ -250,11 +290,11 @@ describe Gitlab::Workhorse do
}
end
- subject { described_class.git_http_ok(repository, false, user, action) }
+ subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) }
it { expect(subject).to include(params) }
- context 'when is_wiki' do
+ context 'when the repo_type is a wiki' do
let(:params) do
{
GL_ID: "user-#{user.id}",
@@ -264,7 +304,7 @@ describe Gitlab::Workhorse do
}
end
- subject { described_class.git_http_ok(repository, true, user, action) }
+ subject { described_class.git_http_ok(repository, Gitlab::GlRepository::WIKI, user, action) }
it { expect(subject).to include(params) }
end
@@ -304,7 +344,7 @@ describe Gitlab::Workhorse do
end
context 'show_all_refs enabled' do
- subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
+ subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) }
it { is_expected.to include(ShowAllRefs: true) }
end
@@ -322,7 +362,7 @@ describe Gitlab::Workhorse do
it { expect(subject).to include(gitaly_params) }
context 'show_all_refs enabled' do
- subject { described_class.git_http_ok(repository, false, user, action, show_all_refs: true) }
+ subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) }
it { is_expected.to include(ShowAllRefs: true) }
end
diff --git a/spec/lib/gitlab_spec.rb b/spec/lib/gitlab_spec.rb
index 5f7a0cca351..e075904b0cc 100644
--- a/spec/lib/gitlab_spec.rb
+++ b/spec/lib/gitlab_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab do
end
describe '.revision' do
- let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h -n 1] }
+ let(:cmd) { %W[#{described_class.config.git.bin_path} log --pretty=format:%h --abbrev=11 -n 1] }
around do |example|
described_class.instance_variable_set(:@_revision, nil)
@@ -95,4 +95,48 @@ describe Gitlab do
expect(described_class.com?).to eq false
end
end
+
+ describe '.ee?' do
+ it 'returns true when using Enterprise Edition' do
+ stub_const('License', Class.new)
+
+ expect(described_class.ee?).to eq(true)
+ end
+
+ it 'returns false when using Community Edition' do
+ hide_const('License')
+
+ expect(described_class.ee?).to eq(false)
+ end
+ end
+
+ describe '.http_proxy_env?' do
+ it 'returns true when lower case https' do
+ stub_env('https_proxy', 'https://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when upper case https' do
+ stub_env('HTTPS_PROXY', 'https://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when lower case http' do
+ stub_env('http_proxy', 'http://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns true when upper case http' do
+ stub_env('HTTP_PROXY', 'http://my.proxy')
+
+ expect(described_class.http_proxy_env?).to eq(true)
+ end
+
+ it 'returns false when not set' do
+ expect(described_class.http_proxy_env?).to eq(false)
+ end
+ end
end
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index e2134dc279c..1fefc947636 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -97,6 +97,12 @@ describe GoogleApi::CloudPlatform::Client do
"node_config": {
"machine_type": machine_type
},
+ "master_auth": {
+ "username": "admin",
+ "client_certificate_config": {
+ issue_client_certificate: true
+ }
+ },
"legacy_abac": {
"enabled": true
}
@@ -122,6 +128,12 @@ describe GoogleApi::CloudPlatform::Client do
"node_config": {
"machine_type": machine_type
},
+ "master_auth": {
+ "username": "admin",
+ "client_certificate_config": {
+ issue_client_certificate: true
+ }
+ },
"legacy_abac": {
"enabled": false
}
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
index 77fea5b2d24..346455067a7 100644
--- a/spec/lib/mattermost/session_spec.rb
+++ b/spec/lib/mattermost/session_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Mattermost::Session, type: :request do
include ExclusiveLeaseHelpers
+ include StubRequests
let(:user) { create(:user) }
@@ -24,7 +25,7 @@ describe Mattermost::Session, type: :request do
let(:location) { 'http://location.tld' }
let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'}
let!(:stub) do
- WebMock.stub_request(:get, "#{mattermost_url}/oauth/gitlab/login")
+ stub_full_request("#{mattermost_url}/oauth/gitlab/login")
.to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 302)
end
@@ -63,7 +64,7 @@ describe Mattermost::Session, type: :request do
end
before do
- WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete")
+ stub_full_request("#{mattermost_url}/signup/gitlab/complete")
.with(query: hash_including({ 'state' => state }))
.to_return do |request|
post "/oauth/token",
@@ -80,7 +81,7 @@ describe Mattermost::Session, type: :request do
end
end
- WebMock.stub_request(:post, "#{mattermost_url}/api/v4/users/logout")
+ stub_full_request("#{mattermost_url}/api/v4/users/logout", method: :post)
.to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
end
diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb
index 1024e1a25ea..8ccbd90ddb8 100644
--- a/spec/lib/object_storage/direct_upload_spec.rb
+++ b/spec/lib/object_storage/direct_upload_spec.rb
@@ -121,7 +121,7 @@ describe ObjectStorage::DirectUpload do
expect(subject[:MultipartUpload][:PartURLs].length).to eq(2)
end
- it 'part size is mimimum, 5MB' do
+ it 'part size is minimum, 5MB' do
expect(subject[:MultipartUpload][:PartSize]).to eq(5.megabyte)
end
end
diff --git a/spec/lib/quality/kubernetes_client_spec.rb b/spec/lib/quality/kubernetes_client_spec.rb
index f35d9464d48..4e77dcc97e6 100644
--- a/spec/lib/quality/kubernetes_client_spec.rb
+++ b/spec/lib/quality/kubernetes_client_spec.rb
@@ -13,7 +13,7 @@ RSpec.describe Quality::KubernetesClient do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with([%(kubectl --namespace "#{namespace}" delete ) \
'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now -l release=\"#{release_name}\""])
+ "--now --ignore-not-found --include-uninitialized -l release=\"#{release_name}\""])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: false)))
expect { subject.cleanup(release_name: release_name) }.to raise_error(described_class::CommandFailedError)
@@ -23,7 +23,7 @@ RSpec.describe Quality::KubernetesClient do
expect(Gitlab::Popen).to receive(:popen_with_detail)
.with([%(kubectl --namespace "#{namespace}" delete ) \
'ingress,svc,pdb,hpa,deploy,statefulset,job,pod,secret,configmap,pvc,secret,clusterrole,clusterrolebinding,role,rolebinding,sa ' \
- "--now -l release=\"#{release_name}\""])
+ "--now --ignore-not-found --include-uninitialized -l release=\"#{release_name}\""])
.and_return(Gitlab::Popen::Result.new([], '', '', double(success?: true)))
# We're not verifying the output here, just silencing it
diff --git a/spec/lib/quality/seeders/issues_spec.rb b/spec/lib/quality/seeders/issues_spec.rb
new file mode 100644
index 00000000000..e17414a541a
--- /dev/null
+++ b/spec/lib/quality/seeders/issues_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Quality::Seeders::Issues do
+ let(:project) { create(:project) }
+
+ subject { described_class.new(project: project) }
+
+ describe '#seed' do
+ it 'seeds issues' do
+ issues_created = subject.seed(backfill_weeks: 1, average_issues_per_week: 1)
+
+ expect(issues_created).to be_between(0, 2)
+ expect(project.issues.count).to eq(issues_created)
+ end
+ end
+end
diff --git a/spec/lib/quality/test_level_spec.rb b/spec/lib/quality/test_level_spec.rb
new file mode 100644
index 00000000000..3465c3a050b
--- /dev/null
+++ b/spec/lib/quality/test_level_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+RSpec.describe Quality::TestLevel do
+ describe '#pattern' do
+ context 'when level is unit' do
+ it 'returns a pattern' do
+ expect(subject.pattern(:unit))
+ .to eq("spec/{bin,config,db,dependencies,factories,finders,frontend,graphql,helpers,initializers,javascripts,lib,migrations,models,policies,presenters,rack_servers,routing,rubocop,serializers,services,sidekiq,tasks,uploaders,validators,views,workers,elastic_integration}{,/**/}*_spec.rb")
+ end
+ end
+
+ context 'when level is integration' do
+ it 'returns a pattern' do
+ expect(subject.pattern(:integration))
+ .to eq("spec/{controllers,mailers,requests}{,/**/}*_spec.rb")
+ end
+ end
+
+ context 'when level is system' do
+ it 'returns a pattern' do
+ expect(subject.pattern(:system))
+ .to eq("spec/{features}{,/**/}*_spec.rb")
+ end
+ end
+
+ context 'with a prefix' do
+ it 'returns a pattern' do
+ expect(described_class.new('ee/').pattern(:system))
+ .to eq("ee/spec/{features}{,/**/}*_spec.rb")
+ end
+ end
+
+ describe 'performance' do
+ it 'memoizes the pattern for a given level' do
+ expect(subject.pattern(:system).object_id).to eq(subject.pattern(:system).object_id)
+ end
+
+ it 'freezes the pattern for a given level' do
+ expect(subject.pattern(:system)).to be_frozen
+ end
+ end
+ end
+
+ describe '#regexp' do
+ context 'when level is unit' do
+ it 'returns a regexp' do
+ expect(subject.regexp(:unit))
+ .to eq(%r{spec/(bin|config|db|dependencies|factories|finders|frontend|graphql|helpers|initializers|javascripts|lib|migrations|models|policies|presenters|rack_servers|routing|rubocop|serializers|services|sidekiq|tasks|uploaders|validators|views|workers|elastic_integration)})
+ end
+ end
+
+ context 'when level is integration' do
+ it 'returns a regexp' do
+ expect(subject.regexp(:integration))
+ .to eq(%r{spec/(controllers|mailers|requests)})
+ end
+ end
+
+ context 'when level is system' do
+ it 'returns a regexp' do
+ expect(subject.regexp(:system))
+ .to eq(%r{spec/(features)})
+ end
+ end
+
+ context 'with a prefix' do
+ it 'returns a regexp' do
+ expect(described_class.new('ee/').regexp(:system))
+ .to eq(%r{ee/spec/(features)})
+ end
+ end
+
+ describe 'performance' do
+ it 'memoizes the regexp for a given level' do
+ expect(subject.regexp(:system).object_id).to eq(subject.regexp(:system).object_id)
+ end
+
+ it 'freezes the regexp for a given level' do
+ expect(subject.regexp(:system)).to be_frozen
+ end
+ end
+ end
+
+ describe '#level_for' do
+ it 'returns the correct level for a unit test' do
+ expect(subject.level_for('spec/models/abuse_report_spec.rb')).to eq(:unit)
+ end
+
+ it 'returns the correct level for an integration test' do
+ expect(subject.level_for('spec/mailers/abuse_report_mailer_spec.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
+
+ it 'raises an error for an unknown level' do
+ expect { subject.level_for('spec/unknown/foo_spec.rb') }
+ .to raise_error(described_class::UnknownTestLevelError,
+ %r{Test level for spec/unknown/foo_spec.rb couldn't be set. Please rename the file properly or change the test level detection regexes in .+/lib/quality/test_level.rb.})
+ end
+ end
+end
diff --git a/spec/lib/sentry/client_spec.rb b/spec/lib/sentry/client_spec.rb
index 88e7e2e5ebb..cb14204b99a 100644
--- a/spec/lib/sentry/client_spec.rb
+++ b/spec/lib/sentry/client_spec.rb
@@ -61,11 +61,37 @@ describe Sentry::Client do
end
end
+ shared_examples 'maps exceptions' do
+ exceptions = {
+ HTTParty::Error => 'Error when connecting to Sentry',
+ Net::OpenTimeout => 'Connection to Sentry timed out',
+ SocketError => 'Received SocketError when trying to connect to Sentry',
+ OpenSSL::SSL::SSLError => 'Sentry returned invalid SSL data',
+ Errno::ECONNREFUSED => 'Connection refused',
+ StandardError => 'Sentry request failed due to StandardError'
+ }
+
+ exceptions.each do |exception, message|
+ context "#{exception}" do
+ before do
+ stub_request(:get, sentry_request_url).to_raise(exception)
+ end
+
+ it do
+ expect { subject }
+ .to raise_exception(Sentry::Client::Error, message)
+ end
+ end
+ end
+ end
+
describe '#list_issues' do
let(:issue_status) { 'unresolved' }
let(:limit) { 20 }
+ let(:sentry_api_response) { issues_sample_response }
+ let(:sentry_request_url) { sentry_url + '/issues/?limit=20&query=is:unresolved' }
- let!(:sentry_api_request) { stub_sentry_request(sentry_url + '/issues/?limit=20&query=is:unresolved', body: issues_sample_response) }
+ let!(:sentry_api_request) { stub_sentry_request(sentry_request_url, body: sentry_api_response) }
subject { client.list_issues(issue_status: issue_status, limit: limit) }
@@ -74,6 +100,14 @@ describe Sentry::Client do
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
it_behaves_like 'has correct length', 1
+ shared_examples 'has correct external_url' do
+ context 'external_url' do
+ it 'is constructed correctly' do
+ expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
+ end
+ end
+ end
+
context 'error object created from sentry response' do
using RSpec::Parameterized::TableSyntax
@@ -96,14 +130,10 @@ describe Sentry::Client do
end
with_them do
- it { expect(subject[0].public_send(error_object)).to eq(issues_sample_response[0].dig(*sentry_response)) }
+ it { expect(subject[0].public_send(error_object)).to eq(sentry_api_response[0].dig(*sentry_response)) }
end
- context 'external_url' do
- it 'is constructed correctly' do
- expect(subject[0].external_url).to eq('https://sentrytest.gitlab.com/sentry-org/sentry-project/issues/11')
- end
- end
+ it_behaves_like 'has correct external_url'
end
context 'redirects' do
@@ -115,16 +145,14 @@ describe Sentry::Client do
# Sentry API returns 404 if there are extra slashes in the URL!
context 'extra slashes in URL' do
let(:sentry_url) { 'https://sentrytest.gitlab.com/api/0/projects//sentry-org/sentry-project/' }
- let(:client) { described_class.new(sentry_url, token) }
- let!(:valid_req_stub) do
- stub_sentry_request(
- 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \
+ let(:sentry_request_url) do
+ 'https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/' \
'issues/?limit=20&query=is:unresolved'
- )
end
it 'removes extra slashes in api url' do
+ expect(client.url).to eq(sentry_url)
expect(Gitlab::HTTP).to receive(:get).with(
URI('https://sentrytest.gitlab.com/api/0/projects/sentry-org/sentry-project/issues/'),
anything
@@ -132,15 +160,47 @@ describe Sentry::Client do
subject
- expect(valid_req_stub).to have_been_requested
+ expect(sentry_api_request).to have_been_requested
end
end
+
+ context 'Older sentry versions where keys are not present' do
+ let(:sentry_api_response) do
+ issues_sample_response[0...1].map do |issue|
+ issue[:project].delete(:id)
+ issue
+ end
+ end
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Error
+ it_behaves_like 'has correct length', 1
+
+ it_behaves_like 'has correct external_url'
+ end
+
+ context 'essential keys missing in API response' do
+ let(:sentry_api_response) do
+ issues_sample_response[0...1].map do |issue|
+ issue.except(:id)
+ end
+ end
+
+ it 'raises exception' do
+ expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
+ end
+ end
+
+ it_behaves_like 'maps exceptions'
end
describe '#list_projects' do
let(:sentry_list_projects_url) { 'https://sentrytest.gitlab.com/api/0/projects/' }
- let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: projects_sample_response) }
+ let(:sentry_api_response) { projects_sample_response }
+
+ let!(:sentry_api_request) { stub_sentry_request(sentry_list_projects_url, body: sentry_api_response) }
subject { client.list_projects }
@@ -149,14 +209,31 @@ describe Sentry::Client do
it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
it_behaves_like 'has correct length', 2
- context 'keys missing in API response' do
- it 'raises exception' do
- projects_sample_response[0].delete(:slug)
+ context 'essential keys missing in API response' do
+ let(:sentry_api_response) do
+ projects_sample_response[0...1].map do |project|
+ project.except(:slug)
+ end
+ end
- stub_sentry_request(sentry_list_projects_url, body: projects_sample_response)
+ it 'raises exception' do
+ expect { subject }.to raise_error(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "slug"')
+ end
+ end
- expect { subject }.to raise_error(Sentry::Client::SentryError, 'Sentry API response is missing keys. key not found: "slug"')
+ context 'optional keys missing in sentry response' do
+ let(:sentry_api_response) do
+ projects_sample_response[0...1].map do |project|
+ project[:organization].delete(:id)
+ project.delete(:id)
+ project.except(:status)
+ end
end
+
+ it_behaves_like 'calls sentry api'
+
+ it_behaves_like 'has correct return type', Gitlab::ErrorTracking::Project
+ it_behaves_like 'has correct length', 1
end
context 'error object created from sentry response' do
@@ -173,7 +250,11 @@ describe Sentry::Client do
end
with_them do
- it { expect(subject[0].public_send(sentry_project_object)).to eq(projects_sample_response[0].dig(*sentry_response)) }
+ it do
+ expect(subject[0].public_send(sentry_project_object)).to(
+ eq(sentry_api_response[0].dig(*sentry_response))
+ )
+ end
end
end
@@ -203,12 +284,18 @@ describe Sentry::Client do
expect(valid_req_stub).to have_been_requested
end
end
+
+ context 'when exception is raised' do
+ let(:sentry_request_url) { sentry_list_projects_url }
+
+ it_behaves_like 'maps exceptions'
+ end
end
private
def stub_sentry_request(url, body: {}, status: 200, headers: {})
- WebMock.stub_request(:get, url)
+ stub_request(:get, url)
.to_return(
status: status,
headers: { 'Content-Type' => 'application/json' }.merge(headers),
diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb
index bda892083b3..f96870cc112 100644
--- a/spec/mailers/abuse_report_mailer_spec.rb
+++ b/spec/mailers/abuse_report_mailer_spec.rb
@@ -4,25 +4,24 @@ describe AbuseReportMailer do
include EmailSpec::Matchers
describe '.notify' do
- context 'with admin_notification_email set' do
- before do
- stub_application_setting(admin_notification_email: 'admin@example.com')
- end
+ before do
+ stub_application_setting(admin_notification_email: 'admin@example.com')
+ end
- it 'sends to the admin_notification_email' do
- report = create(:abuse_report)
+ let(:report) { create(:abuse_report) }
+
+ subject { described_class.notify(report.id) }
- mail = described_class.notify(report.id)
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
- expect(mail).to deliver_to 'admin@example.com'
+ context 'with admin_notification_email set' do
+ it 'sends to the admin_notification_email' do
+ is_expected.to deliver_to 'admin@example.com'
end
it 'includes the user in the subject' do
- report = create(:abuse_report)
-
- mail = described_class.notify(report.id)
-
- expect(mail).to have_subject "#{report.user.name} (#{report.user.username}) was reported for abuse"
+ is_expected.to have_subject "#{report.user.name} (#{report.user.username}) was reported for abuse"
end
end
diff --git a/spec/mailers/email_rejection_mailer_spec.rb b/spec/mailers/email_rejection_mailer_spec.rb
new file mode 100644
index 00000000000..bbe0a50ae8e
--- /dev/null
+++ b/spec/mailers/email_rejection_mailer_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe EmailRejectionMailer do
+ include EmailSpec::Matchers
+
+ describe '#rejection' do
+ let(:raw_email) { 'From: someone@example.com\nraw email here' }
+
+ subject { described_class.rejection('some rejection reason', raw_email) }
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ end
+end
diff --git a/spec/mailers/emails/auto_devops_spec.rb b/spec/mailers/emails/auto_devops_spec.rb
index 839caf3f50e..dd7c12c3143 100644
--- a/spec/mailers/emails/auto_devops_spec.rb
+++ b/spec/mailers/emails/auto_devops_spec.rb
@@ -13,6 +13,9 @@ describe Emails::AutoDevops do
subject { Notify.autodevops_disabled_email(pipeline, owner.email) }
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
it 'sents email with correct subject' do
is_expected.to have_subject("#{project.name} | Auto DevOps pipeline was disabled for #{project.name}")
end
diff --git a/spec/mailers/emails/issues_spec.rb b/spec/mailers/emails/issues_spec.rb
index 09253cf8003..5b5bd6f4308 100644
--- a/spec/mailers/emails/issues_spec.rb
+++ b/spec/mailers/emails/issues_spec.rb
@@ -29,5 +29,14 @@ describe Emails::Issues do
expect(subject).to have_body_text "23, 34, 58"
end
+
+ context 'with header and footer' do
+ let(:results) { { success: 165, error_lines: [], parse_error: false } }
+
+ subject { Notify.import_issues_csv_email(user.id, project.id, results) }
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ end
end
end
diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb
index c74fd66ad22..2f594dbf9d1 100644
--- a/spec/mailers/emails/pages_domains_spec.rb
+++ b/spec/mailers/emails/pages_domains_spec.rb
@@ -5,11 +5,13 @@ describe Emails::PagesDomains do
include EmailSpec::Matchers
include_context 'gitlab email notification'
- set(:project) { create(:project) }
set(:domain) { create(:pages_domain, project: project) }
- set(:user) { project.owner }
+ set(:user) { project.creator }
shared_examples 'a pages domain email' do
+ let(:test_recipient) { user }
+
+ it_behaves_like 'an email sent to a user'
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
@@ -26,6 +28,26 @@ describe Emails::PagesDomains do
end
end
+ shared_examples 'notification about upcoming domain removal' do
+ context 'when domain is not scheduled for removal' do
+ it 'asks user to remove it' do
+ is_expected.to have_body_text 'please remove it'
+ end
+ end
+
+ context 'when domain is scheduled for removal' do
+ before do
+ domain.update!(remove_at: 1.week.from_now)
+ end
+ it 'notifies user that domain will be removed automatically' do
+ aggregate_failures do
+ is_expected.to have_body_text domain.remove_at.strftime('%F %T')
+ is_expected.to have_body_text "it will be removed from your GitLab project"
+ end
+ end
+ end
+ end
+
describe '#pages_domain_enabled_email' do
let(:email_subject) { "#{project.path} | GitLab Pages domain '#{domain.domain}' has been enabled" }
@@ -43,6 +65,8 @@ describe Emails::PagesDomains do
it_behaves_like 'a pages domain email'
+ it_behaves_like 'notification about upcoming domain removal'
+
it { is_expected.to have_body_text 'has been disabled' }
end
@@ -63,6 +87,8 @@ describe Emails::PagesDomains do
it_behaves_like 'a pages domain email'
+ it_behaves_like 'notification about upcoming domain removal'
+
it 'says verification has failed and when the domain is enabled until' do
is_expected.to have_body_text 'Verification has failed'
is_expected.to have_body_text domain.enabled_until.strftime('%F %T')
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 4f578c48d5b..cbbb22ad78c 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -19,7 +19,7 @@ describe Notify do
create(:merge_request, source_project: project,
target_project: project,
author: current_user,
- assignee: assignee,
+ assignees: [assignee],
description: 'Awesome description')
end
@@ -30,8 +30,25 @@ describe Notify do
description: 'My awesome description!')
end
+ describe 'with HTML-encoded entities' do
+ before do
+ described_class.test_email('test@test.com', 'Subject', 'Some body with &mdash;').deliver
+ end
+
+ subject { ActionMailer::Base.deliveries.last }
+
+ it 'retains 7bit encoding' do
+ expect(subject.body.ascii_only?).to eq(true)
+ expect(subject.body.encoding).to eq('7bit')
+ end
+ end
+
context 'for a project' do
shared_examples 'an assignee email' do
+ let(:test_recipient) { assignee }
+
+ it_behaves_like 'an email sent to a user'
+
it 'is sent to the assignee as the author' do
sender = subject.header[:from].addrs.first
@@ -53,6 +70,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject and body' do
aggregate_failures do
@@ -72,6 +91,9 @@ describe Notify do
context 'when sent with a reason' do
subject { described_class.new_issue_email(issue.assignees.first.id, issue.id, NotificationReason::ASSIGNED) }
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
it 'includes the reason in a header' do
is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
end
@@ -99,6 +121,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -118,6 +142,9 @@ describe Notify do
context 'when sent with a reason' do
subject { described_class.reassigned_issue_email(recipient.id, issue.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) }
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
it 'includes the reason in a header' do
is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
end
@@ -134,6 +161,8 @@ describe Notify do
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -173,6 +202,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -194,23 +225,53 @@ describe Notify do
let(:new_issue) { create(:issue) }
subject { described_class.issue_moved_email(recipient, issue, new_issue, current_user) }
- it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
- let(:model) { issue }
- end
- it_behaves_like 'it should show Gmail Actions View Issue link'
- it_behaves_like 'an unsubscribeable thread'
+ context 'when a user has permissions to access the new issue' do
+ before do
+ new_issue.project.add_developer(recipient)
+ end
- it 'contains description about action taken' do
- is_expected.to have_body_text 'Issue was moved to another project'
+ it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+ let(:model) { issue }
+ end
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
+
+ it 'contains description about action taken' do
+ is_expected.to have_body_text 'Issue was moved to another project'
+ end
+
+ it 'has the correct subject and body' do
+ new_issue_url = project_issue_path(new_issue.project, new_issue)
+
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.to have_body_text(new_issue_url)
+ is_expected.to have_body_text(project_issue_path(project, issue))
+ end
+ end
+
+ it 'contains the issue title' do
+ is_expected.to have_body_text new_issue.title
+ end
end
- it 'has the correct subject and body' do
- new_issue_url = project_issue_path(new_issue.project, new_issue)
+ context 'when a user does not permissions to access the new issue' do
+ it 'has the correct subject and body' do
+ new_issue_url = project_issue_path(new_issue.project, new_issue)
- aggregate_failures do
- is_expected.to have_referable_subject(issue, reply: true)
- is_expected.to have_body_text(new_issue_url)
- is_expected.to have_body_text(project_issue_path(project, issue))
+ aggregate_failures do
+ is_expected.to have_referable_subject(issue, reply: true)
+ is_expected.not_to have_body_text(new_issue_url)
+ is_expected.to have_body_text(project_issue_path(project, issue))
+ end
+ end
+
+ it 'does not contain the issue title' do
+ is_expected.not_to have_body_text new_issue.title
+ end
+
+ it 'contains information about missing permissions' do
+ is_expected.to have_body_text "You don't have access to the project."
end
end
end
@@ -218,7 +279,7 @@ describe Notify do
context 'for merge requests' do
describe 'that are new' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread with reply-by-email enabled' do
@@ -226,6 +287,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject and body' do
aggregate_failures do
@@ -241,7 +304,10 @@ describe Notify do
end
context 'when sent with a reason' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id, NotificationReason::ASSIGNED) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id, NotificationReason::ASSIGNED) }
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'includes the reason in a header' do
is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
@@ -262,7 +328,7 @@ describe Notify do
describe 'that are reassigned' do
let(:previous_assignee) { create(:user, name: 'Previous Assignee') }
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
@@ -270,6 +336,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -287,7 +355,10 @@ describe Notify do
end
context 'when sent with a reason' do
- subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::ASSIGNED) }
+ subject { described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::ASSIGNED) }
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'includes the reason in a header' do
is_expected.to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
@@ -297,11 +368,11 @@ describe Notify do
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::ASSIGNED)
is_expected.to have_body_text(text)
- new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, NotificationReason::MENTIONED)
+ new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, NotificationReason::MENTIONED)
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(NotificationReason::MENTIONED)
expect(new_subject).to have_body_text(text)
- new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id, nil)
+ new_subject = described_class.reassigned_merge_request_email(recipient.id, merge_request.id, [previous_assignee.id], current_user.id, nil)
text = EmailsHelper.instance_method(:notification_reason_text).bind(self).call(nil)
expect(new_subject).to have_body_text(text)
end
@@ -309,10 +380,12 @@ describe Notify do
end
describe 'that are new with a description' do
- subject { described_class.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
+ subject { described_class.new_merge_request_email(merge_request.assignee_ids.first, merge_request.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like "an unsubscribeable thread"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains the description' do
is_expected.to have_body_text(merge_request.description)
@@ -329,6 +402,8 @@ describe Notify do
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with a labels subscriptions link in its footer'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -352,6 +427,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -379,6 +456,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
@@ -401,7 +480,7 @@ describe Notify do
source_project: project,
target_project: project,
author: current_user,
- assignee: assignee,
+ assignees: [assignee],
description: 'Awesome description')
end
@@ -413,6 +492,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the merge request author' do
sender = subject.header[:from].addrs[0]
@@ -442,6 +523,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the push user' do
sender = subject.header[:from].addrs[0]
@@ -482,6 +565,9 @@ describe Notify do
subject { described_class.note_issue_email(recipient.id, third_note.id) }
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
it 'has In-Reply-To header pointing to previous note in discussion' do
expect(subject.header['In-Reply-To'].message_ids).to eq(["note_#{second_note.id}@#{host}"])
end
@@ -502,6 +588,9 @@ describe Notify do
subject { described_class.note_issue_email(recipient.id, note.id) }
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
it 'has In-Reply-To header pointing to the issue' do
expect(subject.header['In-Reply-To'].message_ids).to eq(["issue_#{note.noteable.id}@#{host}"])
end
@@ -518,6 +607,9 @@ describe Notify do
subject { described_class.note_project_snippet_email(project_snippet_note.author_id, project_snippet_note.id) }
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
let(:model) { project_snippet }
end
@@ -530,11 +622,15 @@ describe Notify do
end
describe 'project was moved' do
+ let(:test_recipient) { user }
subject { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
+ it_behaves_like 'an email sent to a user'
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject and body' do
is_expected.to have_subject("#{project.name} | Project was moved")
@@ -559,6 +655,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
to_emails = subject.header[:to].addrs.map(&:address)
@@ -582,6 +680,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.full_name} project was denied"
@@ -599,12 +699,16 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{project.full_name} project was granted"
is_expected.to have_body_text project.full_name
is_expected.to have_body_text project.web_url
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)
end
end
@@ -629,6 +733,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{project.full_name} project"
@@ -653,6 +759,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
@@ -671,11 +779,13 @@ describe Notify do
invitee
end
- subject { described_class.member_invite_declined_email('project', project.id, project_member.invite_email, maintainer.id) }
+ subject { described_class.member_invite_declined_email('Project', project.id, project_member.invite_email, maintainer.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
@@ -708,6 +818,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject and body' do
aggregate_failures do
@@ -732,6 +844,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject and body' do
aggregate_failures do
@@ -756,6 +870,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject and body' do
aggregate_failures do
@@ -819,6 +935,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject' do
is_expected.to have_subject "Re: #{project.name} | #{commit.title} (#{commit.short_id})"
@@ -845,6 +963,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject' do
is_expected.to have_referable_subject(merge_request, reply: true)
@@ -871,6 +991,8 @@ describe Notify do
end
it_behaves_like 'it should show Gmail Actions View Issue link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject' do
is_expected.to have_referable_subject(issue, reply: true)
@@ -948,6 +1070,8 @@ describe Notify do
it_behaves_like 'an email for a note on a diff discussion', :diff_note_on_commit
it_behaves_like 'it should show Gmail Actions View Commit link'
it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
end
describe 'on a merge request' do
@@ -958,13 +1082,13 @@ describe Notify do
it_behaves_like 'an email for a note on a diff discussion', :diff_note_on_merge_request
it_behaves_like 'it should show Gmail Actions View Merge request link'
it_behaves_like 'an unsubscribeable thread'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
end
end
end
context 'for a group' do
- set(:group) { create(:group) }
-
describe 'group access requested' do
let(:group) { create(:group, :public, :access_requestable) }
let(:group_member) do
@@ -976,6 +1100,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
to_emails = subject.header[:to].addrs.map(&:address)
@@ -998,6 +1124,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was denied"
@@ -1014,12 +1142,16 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject "Access to the #{group.name} group was granted"
is_expected.to have_body_text group.name
is_expected.to have_body_text group.web_url
is_expected.to have_body_text group_member.human_access
+ is_expected.to have_body_text 'leave the group'
+ is_expected.to have_body_text group_url(group, leave: 1)
end
end
@@ -1044,6 +1176,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject "Invitation to join the #{group.name} group"
@@ -1068,6 +1202,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
@@ -1091,6 +1227,8 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
@@ -1140,6 +1278,8 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -1165,6 +1305,8 @@ describe Notify do
it_behaves_like "a user cannot unsubscribe through footer link"
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -1189,6 +1331,8 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -1210,6 +1354,8 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -1237,6 +1383,8 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -1328,6 +1476,8 @@ describe Notify do
it_behaves_like 'a user cannot unsubscribe through footer link'
it_behaves_like 'an email with X-GitLab headers containing project details'
it_behaves_like 'an email that contains a header with author username'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -1348,6 +1498,11 @@ describe Notify do
describe 'HTML emails setting' do
let(:multipart_mail) { described_class.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
+ subject { multipart_mail }
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+
context 'when disabled' do
it 'only sends the text template' do
stub_application_setting(html_emails_enabled: false)
@@ -1386,6 +1541,8 @@ describe Notify do
subject { described_class.note_personal_snippet_email(personal_snippet_note.author_id, personal_snippet_note.id) }
it_behaves_like 'a user cannot unsubscribe through footer link'
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
it 'has the correct subject and body' do
is_expected.to have_referable_subject(personal_snippet, reply: true)
diff --git a/spec/mailers/repository_check_mailer_spec.rb b/spec/mailers/repository_check_mailer_spec.rb
index 00613c7b671..384660f7221 100644
--- a/spec/mailers/repository_check_mailer_spec.rb
+++ b/spec/mailers/repository_check_mailer_spec.rb
@@ -17,5 +17,12 @@ describe RepositoryCheckMailer do
expect(mail).to have_subject 'GitLab Admin | 3 projects failed their last repository check'
end
+
+ context 'with footer and header' do
+ subject { described_class.notify(1) }
+
+ it_behaves_like 'appearance header and footer enabled'
+ it_behaves_like 'appearance header and footer not enabled'
+ end
end
end
diff --git a/spec/migrations/add_foreign_keys_to_todos_spec.rb b/spec/migrations/add_foreign_keys_to_todos_spec.rb
index efd87173b9c..2500e2f8333 100644
--- a/spec/migrations/add_foreign_keys_to_todos_spec.rb
+++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb
@@ -36,7 +36,7 @@ describe AddForeignKeysToTodos, :migration do
end
context 'add foreign key on note_id' do
- let(:note) { create(:note) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let(:note) { table(:notes).create! }
let!(:todo_with_note) { create_todo(note_id: note.id) }
let!(:todo_with_invalid_note) { create_todo(note_id: 4711) }
let!(:todo_without_note) { create_todo(note_id: nil) }
diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
index d8dd7a2fb83..13dc62595b5 100644
--- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
+++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb
@@ -1,23 +1,21 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb')
-describe AddHeadPipelineForEachMergeRequest, :delete do
- include ProjectForksHelper
-
+describe AddHeadPipelineForEachMergeRequest, :migration do
let(:migration) { described_class.new }
- let!(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:other_project) { fork_project(project) }
+ let!(:project) { table(:projects).create! }
+ let!(:other_project) { table(:projects).create! }
- let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:pipeline_3) { create(:ci_pipeline, project: other_project, ref: "branch_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:pipeline_4) { create(:ci_pipeline, project: project, ref: "branch_2") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:pipeline_1) { table(:ci_pipelines).create!(project_id: project.id, ref: "branch_1") }
+ let!(:pipeline_2) { table(:ci_pipelines).create!(project_id: other_project.id, ref: "branch_1") }
+ let!(:pipeline_3) { table(:ci_pipelines).create!(project_id: other_project.id, ref: "branch_1") }
+ let!(:pipeline_4) { table(:ci_pipelines).create!(project_id: project.id, ref: "branch_2") }
- let!(:mr_1) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_1", target_branch: "target_1") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:mr_2) { create(:merge_request, source_project: other_project, target_project: project, source_branch: "branch_1", target_branch: "target_2") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:mr_3) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_2", target_branch: "master") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:mr_4) { create(:merge_request, source_project: project, target_project: project, source_branch: "branch_3", target_branch: "master") } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:mr_1) { table(:merge_requests).create!(source_project_id: project.id, target_project_id: project.id, source_branch: "branch_1", target_branch: "target_1") }
+ let!(:mr_2) { table(:merge_requests).create!(source_project_id: other_project.id, target_project_id: project.id, source_branch: "branch_1", target_branch: "target_2") }
+ let!(:mr_3) { table(:merge_requests).create!(source_project_id: project.id, target_project_id: project.id, source_branch: "branch_2", target_branch: "master") }
+ let!(:mr_4) { table(:merge_requests).create!(source_project_id: project.id, target_project_id: project.id, source_branch: "branch_3", target_branch: "master") }
context "#up" do
context "when source_project and source_branch of pipeline are the same of merge request" do
diff --git a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
index 19f06810e54..09c78d02890 100644
--- a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
+++ b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
@@ -3,12 +3,30 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170803090603_calculate_conv_dev_index_percentages.rb')
-describe CalculateConvDevIndexPercentages, :delete do
+describe CalculateConvDevIndexPercentages, :migration do
let(:migration) { described_class.new }
let!(:conv_dev_index) do
- create(:conversational_development_index_metric, # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ table(:conversational_development_index_metrics).create!(
+ leader_issues: 9.256,
leader_notes: 0,
+ leader_milestones: 16.2456,
+ leader_boards: 5.2123,
+ leader_merge_requests: 1.2,
+ leader_ci_pipelines: 12.1234,
+ leader_environments: 3.3333,
+ leader_deployments: 1.200,
+ leader_projects_prometheus_active: 0.111,
+ leader_service_desk_issues: 15.891,
+ instance_issues: 1.234,
+ instance_notes: 28.123,
instance_milestones: 0,
+ instance_boards: 3.254,
+ instance_merge_requests: 0.6,
+ instance_ci_pipelines: 2.344,
+ instance_environments: 2.2222,
+ instance_deployments: 0.771,
+ instance_projects_prometheus_active: 0.109,
+ instance_service_desk_issues: 13.345,
percentage_issues: 0,
percentage_notes: 0,
percentage_milestones: 0,
diff --git a/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb b/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb
new file mode 100644
index 00000000000..93e7e9304b1
--- /dev/null
+++ b/spec/migrations/change_packages_size_defaults_in_project_statistics_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20190516155724_change_packages_size_defaults_in_project_statistics.rb')
+
+describe ChangePackagesSizeDefaultsInProjectStatistics, :migration do
+ let(:project_statistics) { table(:project_statistics) }
+ let(:projects) { table(:projects) }
+
+ it 'removes null packages_size' do
+ stats_to_migrate = 10
+
+ stats_to_migrate.times do |i|
+ p = projects.create!(name: "project #{i}", namespace_id: 1)
+ project_statistics.create!(project_id: p.id, namespace_id: p.namespace_id)
+ end
+
+ expect { migrate! }
+ .to change { ProjectStatistics.where(packages_size: nil).count }
+ .from(stats_to_migrate)
+ .to(0)
+ end
+
+ it 'defaults packages_size to 0' do
+ project = projects.create!(name: 'a new project', namespace_id: 2)
+ stat = project_statistics.create!(project_id: project.id, namespace_id: project.namespace_id)
+
+ expect(stat.packages_size).to be_nil
+
+ migrate!
+
+ stat.reload
+ expect(stat.packages_size).to eq(0)
+ end
+end
diff --git a/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb b/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb
new file mode 100644
index 00000000000..572b7dfd0c8
--- /dev/null
+++ b/spec/migrations/clean_up_noteable_id_for_notes_on_commits_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190313092516_clean_up_noteable_id_for_notes_on_commits.rb')
+
+describe CleanUpNoteableIdForNotesOnCommits, :migration do
+ let(:notes) { table(:notes) }
+
+ before do
+ notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test')
+ notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test')
+ notes.create!(noteable_type: 'Commit', commit_id: '3d0a182204cece4857f81c6462720e0ad1af39c9', noteable_id: 3, note: 'Test')
+
+ notes.create!(noteable_type: 'Issue', noteable_id: 1, note: 'Test')
+ notes.create!(noteable_type: 'MergeRequest', noteable_id: 1, note: 'Test')
+ notes.create!(noteable_type: 'Snippet', noteable_id: 1, note: 'Test')
+ end
+
+ it 'clears noteable_id for notes on commits' do
+ expect { migrate! }.to change { dirty_notes_on_commits.count }.from(3).to(0)
+ end
+
+ it 'does not clear noteable_id for other notes' do
+ expect { migrate! }.not_to change { other_notes.count }
+ end
+
+ def dirty_notes_on_commits
+ notes.where(noteable_type: 'Commit').where('noteable_id IS NOT NULL')
+ end
+
+ def other_notes
+ notes.where("noteable_type != 'Commit' AND noteable_id IS NOT NULL")
+ end
+end
diff --git a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb
index 8f40ac3e38b..0e6bded29b4 100644
--- a/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb
+++ b/spec/migrations/cleanup_nonexisting_namespace_pending_delete_projects_spec.rb
@@ -1,20 +1,17 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170816102555_cleanup_nonexisting_namespace_pending_delete_projects.rb')
-describe CleanupNonexistingNamespacePendingDeleteProjects do
- before do
- # Stub after_save callbacks that will fail when Project has invalid namespace
- allow_any_instance_of(Project).to receive(:ensure_storage_path_exist).and_return(nil)
- allow_any_instance_of(Project).to receive(:update_project_statistics).and_return(nil)
- end
+describe CleanupNonexistingNamespacePendingDeleteProjects, :migration do
+ let(:projects) { table(:projects) }
+ let(:namespaces) { table(:namespaces) }
describe '#up' do
- set(:some_project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:some_project) { projects.create! }
+ let(:namespace) { namespaces.create!(name: 'test', path: 'test') }
it 'only cleans up when namespace does not exist' do
- create(:project, pending_delete: true) # rubocop:disable RSpec/FactoriesInMigrationSpecs
- project = build(:project, pending_delete: true, namespace: nil, namespace_id: Namespace.maximum(:id).to_i.succ) # rubocop:disable RSpec/FactoriesInMigrationSpecs
- project.save(validate: false)
+ projects.create!(pending_delete: true, namespace_id: namespace.id)
+ project = projects.create!(pending_delete: true, namespace_id: 0)
expect(NamespacelessProjectDestroyWorker).to receive(:bulk_perform_async).with([[project.id]])
@@ -22,7 +19,7 @@ describe CleanupNonexistingNamespacePendingDeleteProjects do
end
it 'does nothing when no pending delete projects without namespace found' do
- create(:project, pending_delete: true, namespace: create(:namespace)) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ projects.create!(pending_delete: true, namespace_id: namespace.id)
expect(NamespacelessProjectDestroyWorker).not_to receive(:bulk_perform_async)
diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
index e2ce69a7bb1..58b8b4a16f0 100644
--- a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
+++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
@@ -1,25 +1,36 @@
# frozen_string_literal: true
-# rubocop:disable RSpec/FactoriesInMigrationSpecs
+
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180723130817_delete_inconsistent_internal_id_records.rb')
describe DeleteInconsistentInternalIdRecords, :migration do
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project) }
- let!(:project3) { create(:project) }
+ let!(:namespace) { table(:namespaces).create!(name: 'test', path: 'test') }
+ let!(:project1) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:project2) { table(:projects).create!(namespace_id: namespace.id) }
+ let!(:project3) { table(:projects).create!(namespace_id: namespace.id) }
- let(:internal_id_query) { ->(project) { InternalId.where(usage: InternalId.usages[scope.to_s.tableize], project: project) } }
+ let(:internal_ids) { table(:internal_ids) }
+ let(:internal_id_query) { ->(project) { InternalId.where(usage: InternalId.usages[scope.to_s.tableize], project_id: project.id) } }
let(:create_models) do
- 3.times { create(scope, project: project1) }
- 3.times { create(scope, project: project2) }
- 3.times { create(scope, project: project3) }
+ [project1, project2, project3].each do |project|
+ 3.times do |i|
+ attributes = required_attributes.merge(project_id: project.id,
+ iid: i.succ)
+
+ table(scope.to_s.pluralize).create!(attributes)
+ end
+ end
end
shared_examples_for 'deleting inconsistent internal_id records' do
before do
create_models
+ [project1, project2, project3].each do |project|
+ internal_ids.create!(project_id: project.id, usage: InternalId.usages[scope.to_s.tableize], last_value: 3)
+ end
+
internal_id_query.call(project1).first.tap do |iid|
iid.last_value = iid.last_value - 2
# This is an inconsistent record
@@ -33,11 +44,11 @@ describe DeleteInconsistentInternalIdRecords, :migration do
end
end
- it "deletes inconsistent issues" do
+ it "deletes inconsistent records" do
expect { migrate! }.to change { internal_id_query.call(project1).size }.from(1).to(0)
end
- it "retains consistent issues" do
+ it "retains consistent records" do
expect { migrate! }.not_to change { internal_id_query.call(project2).size }
end
@@ -48,6 +59,8 @@ describe DeleteInconsistentInternalIdRecords, :migration do
context 'for issues' do
let(:scope) { :issue }
+ let(:required_attributes) { {} }
+
it_behaves_like 'deleting inconsistent internal_id records'
end
@@ -55,9 +68,17 @@ describe DeleteInconsistentInternalIdRecords, :migration do
let(:scope) { :merge_request }
let(:create_models) do
- 3.times { |i| create(scope, target_project: project1, source_project: project1, source_branch: i.to_s) }
- 3.times { |i| create(scope, target_project: project2, source_project: project2, source_branch: i.to_s) }
- 3.times { |i| create(scope, target_project: project3, source_project: project3, source_branch: i.to_s) }
+ [project1, project2, project3].each do |project|
+ 3.times do |i|
+ table(:merge_requests).create!(
+ target_project_id: project.id,
+ source_project_id: project.id,
+ target_branch: 'master',
+ source_branch: j.to_s,
+ iid: i.succ
+ )
+ end
+ end
end
it_behaves_like 'deleting inconsistent internal_id records'
@@ -66,13 +87,6 @@ describe DeleteInconsistentInternalIdRecords, :migration do
context 'for deployments' do
let(:scope) { :deployment }
let(:deployments) { table(:deployments) }
- let(:internal_ids) { table(:internal_ids) }
-
- before do
- internal_ids.create!(project_id: project1.id, usage: 2, last_value: 2)
- internal_ids.create!(project_id: project2.id, usage: 2, last_value: 2)
- internal_ids.create!(project_id: project3.id, usage: 2, last_value: 2)
- end
let(:create_models) do
3.times { |i| deployments.create!(project_id: project1.id, iid: i, environment_id: 1, ref: 'master', sha: 'a', tag: false) }
@@ -85,17 +99,14 @@ describe DeleteInconsistentInternalIdRecords, :migration do
context 'for milestones (by project)' do
let(:scope) { :milestone }
+ let(:required_attributes) { { title: 'test' } }
+
it_behaves_like 'deleting inconsistent internal_id records'
end
context 'for ci_pipelines' do
let(:scope) { :ci_pipeline }
-
- let(:create_models) do
- create_list(:ci_empty_pipeline, 3, project: project1)
- create_list(:ci_empty_pipeline, 3, project: project2)
- create_list(:ci_empty_pipeline, 3, project: project3)
- end
+ let(:required_attributes) { { ref: 'test' } }
it_behaves_like 'deleting inconsistent internal_id records'
end
@@ -107,12 +118,20 @@ describe DeleteInconsistentInternalIdRecords, :migration do
let(:group2) { groups.create(name: 'Group 2', type: 'Group', path: 'group_2') }
let(:group3) { groups.create(name: 'Group 2', type: 'Group', path: 'group_3') }
- let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace: group) } }
+ let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace_id: group.id) } }
before do
- 3.times { create(:milestone, group_id: group1.id) }
- 3.times { create(:milestone, group_id: group2.id) }
- 3.times { create(:milestone, group_id: group3.id) }
+ [group1, group2, group3].each do |group|
+ 3.times do |i|
+ table(:milestones).create!(
+ group_id: group.id,
+ title: 'test',
+ iid: i.succ
+ )
+ end
+
+ internal_ids.create!(namespace_id: group.id, usage: InternalId.usages['milestones'], last_value: 3)
+ end
internal_id_query.call(group1).first.tap do |iid|
iid.last_value = iid.last_value - 2
@@ -127,11 +146,11 @@ describe DeleteInconsistentInternalIdRecords, :migration do
end
end
- it "deletes inconsistent issues" do
+ it "deletes inconsistent records" do
expect { migrate! }.to change { internal_id_query.call(group1).size }.from(1).to(0)
end
- it "retains consistent issues" do
+ it "retains consistent records" do
expect { migrate! }.not_to change { internal_id_query.call(group2).size }
end
diff --git a/spec/migrations/enqueue_reset_merge_status_spec.rb b/spec/migrations/enqueue_reset_merge_status_spec.rb
new file mode 100644
index 00000000000..0d5e33bfd46
--- /dev/null
+++ b/spec/migrations/enqueue_reset_merge_status_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190528180441_enqueue_reset_merge_status.rb')
+
+describe EnqueueResetMergeStatus, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id, extra_params = {})
+ params = {
+ id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}"
+ }.merge(extra_params)
+
+ merge_requests.create!(params)
+ end
+
+ it 'correctly schedules background migrations' do
+ create_merge_request(1, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(2, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(3, state: 'opened', merge_status: 'can_be_merged')
+ create_merge_request(4, state: 'merged', merge_status: 'can_be_merged')
+ create_merge_request(5, state: 'opened', merge_status: 'unchecked')
+
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(5.minutes, 1, 2)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(10.minutes, 3, 3)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb b/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
index afcaefa0591..abf39317188 100644
--- a/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
+++ b/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb
@@ -8,9 +8,13 @@ describe EnqueueVerifyPagesDomainWorkers, :sidekiq, :migration do
end
end
+ let(:domains_table) { table(:pages_domains) }
+
describe '#up' do
it 'enqueues a verification worker for every domain' do
- domains = 1.upto(3).map { |i| PagesDomain.create!(domain: "my#{i}.domain.com") }
+ domains = Array.new(3) do |i|
+ domains_table.create!(domain: "my#{i}.domain.com", verification_code: "123#{i}")
+ end
expect { migrate! }.to change(PagesDomainVerificationWorker.jobs, :size).by(3)
diff --git a/spec/migrations/generate_lets_encrypt_private_key_spec.rb b/spec/migrations/generate_lets_encrypt_private_key_spec.rb
new file mode 100644
index 00000000000..773bf5222f0
--- /dev/null
+++ b/spec/migrations/generate_lets_encrypt_private_key_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20190524062810_generate_lets_encrypt_private_key.rb')
+
+describe GenerateLetsEncryptPrivateKey, :migration do
+ describe '#up' do
+ it 'does not fail' do
+ expect do
+ described_class.new.up
+ end.not_to raise_error
+ end
+ end
+end
diff --git a/spec/migrations/generate_missing_routes_spec.rb b/spec/migrations/generate_missing_routes_spec.rb
index 32515d353b0..30ad135d4df 100644
--- a/spec/migrations/generate_missing_routes_spec.rb
+++ b/spec/migrations/generate_missing_routes_spec.rb
@@ -8,7 +8,7 @@ describe GenerateMissingRoutes, :migration do
let(:routes) { table(:routes) }
it 'creates routes for projects without a route' do
- namespace = namespaces.create!(name: 'GitLab', path: 'gitlab')
+ namespace = namespaces.create!(name: 'GitLab', path: 'gitlab', type: 'Group')
routes.create!(
path: 'gitlab',
diff --git a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
index 495e86ee888..71a4e71ac8a 100644
--- a/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
+++ b/spec/migrations/issues_moved_to_id_foreign_key_spec.rb
@@ -1,20 +1,19 @@
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20171106151218_issues_moved_to_id_foreign_key.rb')
-# The schema version has to be far enough in advance to have the
-# only_mirror_protected_branches column in the projects table to create a
-# project via FactoryBot.
-describe IssuesMovedToIdForeignKey, :migration, schema: 20171114150259 do
- let!(:issue_first) { create(:issue, moved_to_id: issue_second.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:issue_second) { create(:issue, moved_to_id: issue_third.id) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:issue_third) { create(:issue) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+describe IssuesMovedToIdForeignKey, :migration do
+ let(:issues) { table(:issues) }
+
+ let!(:issue_third) { issues.create! }
+ let!(:issue_second) { issues.create!(moved_to_id: issue_third.id) }
+ let!(:issue_first) { issues.create!(moved_to_id: issue_second.id) }
subject { described_class.new }
it 'removes the orphaned moved_to_id' do
subject.down
- issue_third.update(moved_to_id: 100000)
+ issue_third.update!(moved_to_id: 0)
subject.up
diff --git a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
index b1ff3cfd355..349cffea70e 100644
--- a/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
+++ b/spec/migrations/migrate_auto_dev_ops_domain_to_cluster_domain_spec.rb
@@ -25,7 +25,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do
context 'with ProjectAutoDevOps with no domain' do
let(:domain) { nil }
- it 'should not update cluster project' do
+ it 'does not update cluster project' do
migrate!
expect(clusters_without_domain.count).to eq(clusters_table.count)
@@ -35,7 +35,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do
context 'with ProjectAutoDevOps with domain' do
let(:domain) { 'example-domain.com' }
- it 'should update all cluster projects' do
+ it 'updates all cluster projects' do
migrate!
expect(clusters_with_domain.count).to eq(clusters_table.count)
@@ -49,7 +49,7 @@ describe MigrateAutoDevOpsDomainToClusterDomain, :migration do
setup_cluster_projects_with_domain(quantity: 25, domain: nil)
end
- it 'should only update specific cluster projects' do
+ it 'only updates specific cluster projects' do
migrate!
expect(clusters_with_domain.count).to eq(20)
diff --git a/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb b/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb
index b2d8f476bb2..a1f243651b5 100644
--- a/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_cluster_configure_worker_sidekiq_queue_spec.rb
@@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20181219145520_migrate_cluster_co
describe MigrateClusterConfigureWorkerSidekiqQueue, :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
- stubbed_worker(queue: 'gcp_cluster:cluster_platform_configure').perform_async('Something', [1])
- stubbed_worker(queue: 'gcp_cluster:cluster_configure').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:cluster_platform_configure').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:cluster_configure').perform_async('Something', [1])
described_class.new.up
@@ -19,12 +20,12 @@ describe MigrateClusterConfigureWorkerSidekiqQueue, :sidekiq, :redis do
it 'does not affect other queues under the same namespace' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: 'gcp_cluster:cluster_install_app').perform_async('Something', [1])
- stubbed_worker(queue: 'gcp_cluster:cluster_provision').perform_async('Something', [1])
- stubbed_worker(queue: 'gcp_cluster:cluster_wait_for_app_installation').perform_async('Something', [1])
- stubbed_worker(queue: 'gcp_cluster:wait_for_cluster_creation').perform_async('Something', [1])
- stubbed_worker(queue: 'gcp_cluster:cluster_wait_for_ingress_ip_address').perform_async('Something', [1])
- stubbed_worker(queue: 'gcp_cluster:cluster_project_configure').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:cluster_install_app').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:cluster_provision').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:cluster_wait_for_app_installation').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:wait_for_cluster_creation').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:cluster_wait_for_ingress_ip_address').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:cluster_project_configure').perform_async('Something', [1])
described_class.new.up
@@ -39,7 +40,7 @@ describe MigrateClusterConfigureWorkerSidekiqQueue, :sidekiq, :redis do
it 'correctly migrates queue when migrating down' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: 'gcp_cluster:cluster_configure').perform_async('Something', [1])
+ stub_worker(queue: 'gcp_cluster:cluster_configure').perform_async('Something', [1])
described_class.new.down
@@ -58,11 +59,4 @@ describe MigrateClusterConfigureWorkerSidekiqQueue, :sidekiq, :redis do
expect { described_class.new.down }.not_to raise_error
end
end
-
- def stubbed_worker(queue:)
- Class.new do
- include Sidekiq::Worker
- sidekiq_options queue: queue
- end
- end
end
diff --git a/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb b/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb
index c18ae3b76d3..66555118a43 100644
--- a/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_create_trace_artifact_sidekiq_queue_spec.rb
@@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20180306074045_migrate_create_tra
describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
+ include StubWorker
context 'when there are jobs in the queues' do
it 'correctly migrates queue when migrating up' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: 'pipeline_default:create_trace_artifact').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:create_trace_artifact').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1])
described_class.new.up
@@ -19,11 +20,11 @@ describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do
it 'does not affect other queues under the same namespace' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1])
described_class.new.up
@@ -37,7 +38,7 @@ describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do
it 'correctly migrates queue when migrating down' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1])
described_class.new.down
@@ -56,11 +57,4 @@ describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do
expect { described_class.new.down }.not_to raise_error
end
end
-
- def stubbed_worker(queue:)
- Class.new do
- include Sidekiq::Worker
- sidekiq_options queue: queue
- end
- end
end
diff --git a/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb
index 1ee6c440cf4..6ce04805e5d 100644
--- a/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_object_storage_upload_sidekiq_queue_spec.rb
@@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20180603190921_migrate_object_sto
describe MigrateObjectStorageUploadSidekiqQueue, :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
- stubbed_worker(queue: 'object_storage_upload').perform_async('Something', [1])
- stubbed_worker(queue: 'object_storage:object_storage_background_move').perform_async('Something', [1])
+ stub_worker(queue: 'object_storage_upload').perform_async('Something', [1])
+ stub_worker(queue: 'object_storage:object_storage_background_move').perform_async('Something', [1])
described_class.new.up
@@ -23,11 +24,4 @@ describe MigrateObjectStorageUploadSidekiqQueue, :sidekiq, :redis do
expect { described_class.new.up }.not_to raise_error
end
end
-
- def stubbed_worker(queue:)
- Class.new do
- include Sidekiq::Worker
- sidekiq_options queue: queue
- end
- end
end
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
index af77d64fdbf..bc826d91471 100644
--- a/spec/migrations/migrate_old_artifacts_spec.rb
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -3,7 +3,9 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170523083112_migrate_old_artifacts.rb')
-describe MigrateOldArtifacts do
+# Adding the ci_job_artifacts table (from the 20170918072948 schema)
+# makes the use of model code below easier.
+describe MigrateOldArtifacts, :migration, schema: 20170918072948 do
let(:migration) { described_class.new }
let!(:directory) { Dir.mktmpdir }
@@ -16,18 +18,22 @@ describe MigrateOldArtifacts do
end
context 'with migratable data' do
- set(:project1) { create(:project, ci_id: 2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- set(:project2) { create(:project, ci_id: 3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- set(:project3) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let(:projects) { table(:projects) }
+ let(:ci_pipelines) { table(:ci_pipelines) }
+ let(:ci_builds) { table(:ci_builds) }
- set(:pipeline1) { create(:ci_empty_pipeline, project: project1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- set(:pipeline2) { create(:ci_empty_pipeline, project: project2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- set(:pipeline3) { create(:ci_empty_pipeline, project: project3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:project1) { projects.create!(ci_id: 2) }
+ let!(:project2) { projects.create!(ci_id: 3) }
+ let!(:project3) { projects.create! }
- let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:build2) { create(:ci_build, pipeline: pipeline2) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:build3) { create(:ci_build, pipeline: pipeline3) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:pipeline1) { ci_pipelines.create!(project_id: project1.id) }
+ let!(:pipeline2) { ci_pipelines.create!(project_id: project2.id) }
+ let!(:pipeline3) { ci_pipelines.create!(project_id: project3.id) }
+
+ let!(:build_with_legacy_artifacts) { ci_builds.create!(commit_id: pipeline1.id, project_id: project1.id, type: 'Ci::Build').becomes(Ci::Build) }
+ let!(:build_without_artifacts) { ci_builds.create!(commit_id: pipeline1.id, project_id: project1.id, type: 'Ci::Build').becomes(Ci::Build) }
+ let!(:build2) { ci_builds.create!(commit_id: pipeline2.id, project_id: project2.id, type: 'Ci::Build').becomes(Ci::Build) }
+ let!(:build3) { ci_builds.create!(commit_id: pipeline3.id, project_id: project3.id, type: 'Ci::Build').becomes(Ci::Build) }
before do
setup_builds(build2, build3)
@@ -39,10 +45,6 @@ describe MigrateOldArtifacts do
expect(build_with_legacy_artifacts.artifacts?).to be_falsey
end
- it "legacy artifacts are set" do
- expect(build_with_legacy_artifacts.legacy_artifacts_file_identifier).not_to be_nil
- end
-
describe '#min_id' do
subject { migration.send(:min_id) }
diff --git a/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb b/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb
index e02bcd2f4da..e38044ccceb 100644
--- a/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb
+++ b/spec/migrations/migrate_pipeline_sidekiq_queues_spec.rb
@@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20170822101017_migrate_pipeline_s
describe MigratePipelineSidekiqQueues, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
+ include StubWorker
context 'when there are jobs in the queues' do
it 'correctly migrates queue when migrating up' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: :pipeline).perform_async('Something', [1])
- stubbed_worker(queue: :build).perform_async('Something', [1])
+ stub_worker(queue: :pipeline).perform_async('Something', [1])
+ stub_worker(queue: :build).perform_async('Something', [1])
described_class.new.up
@@ -20,10 +21,10 @@ describe MigratePipelineSidekiqQueues, :sidekiq, :redis do
it 'correctly migrates queue when migrating down' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: :pipeline_default).perform_async('Class', [1])
- stubbed_worker(queue: :pipeline_processing).perform_async('Class', [2])
- stubbed_worker(queue: :pipeline_hooks).perform_async('Class', [3])
- stubbed_worker(queue: :pipeline_cache).perform_async('Class', [4])
+ stub_worker(queue: :pipeline_default).perform_async('Class', [1])
+ stub_worker(queue: :pipeline_processing).perform_async('Class', [2])
+ stub_worker(queue: :pipeline_hooks).perform_async('Class', [3])
+ stub_worker(queue: :pipeline_cache).perform_async('Class', [4])
described_class.new.down
@@ -45,11 +46,4 @@ describe MigratePipelineSidekiqQueues, :sidekiq, :redis do
expect { described_class.new.down }.not_to raise_error
end
end
-
- def stubbed_worker(queue:)
- Class.new do
- include Sidekiq::Worker
- sidekiq_options queue: queue
- end
- end
end
diff --git a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
index f8cf76cb339..94de208e53e 100644
--- a/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_storage_migrator_sidekiq_queue_spec.rb
@@ -3,11 +3,12 @@ require Rails.root.join('db', 'post_migrate', '20190124200344_migrate_storage_mi
describe MigrateStorageMigratorSidekiqQueue, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
+ include StubWorker
context 'when there are jobs in the queues' do
it 'correctly migrates queue when migrating up' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: :storage_migrator).perform_async(1, 5)
+ stub_worker(queue: :storage_migrator).perform_async(1, 5)
described_class.new.up
@@ -18,7 +19,7 @@ describe MigrateStorageMigratorSidekiqQueue, :sidekiq, :redis do
it 'correctly migrates queue when migrating down' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: :'hashed_storage:hashed_storage_migrator').perform_async(1, 5)
+ stub_worker(queue: :'hashed_storage:hashed_storage_migrator').perform_async(1, 5)
described_class.new.down
@@ -37,11 +38,4 @@ describe MigrateStorageMigratorSidekiqQueue, :sidekiq, :redis do
expect { described_class.new.down }.not_to raise_error
end
end
-
- def stubbed_worker(queue:)
- Class.new do
- include Sidekiq::Worker
- sidekiq_options queue: queue
- end
- end
end
diff --git a/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb b/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb
index 5e3b20ab4a8..976f3ce07d7 100644
--- a/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb
+++ b/spec/migrations/migrate_update_head_pipeline_for_merge_request_sidekiq_queue_spec.rb
@@ -3,12 +3,13 @@ require Rails.root.join('db', 'post_migrate', '20180307012445_migrate_update_hea
describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
+ include StubWorker
context 'when there are jobs in the queues' do
it 'correctly migrates queue when migrating up' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1])
described_class.new.up
@@ -19,10 +20,10 @@ describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis
it 'does not affect other queues under the same namespace' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1])
- stubbed_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1])
described_class.new.up
@@ -35,7 +36,7 @@ describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis
it 'correctly migrates queue when migrating down' do
Sidekiq::Testing.disable! do
- stubbed_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1])
+ stub_worker(queue: 'pipeline_processing:update_head_pipeline_for_merge_request').perform_async('Something', [1])
described_class.new.down
@@ -54,11 +55,4 @@ describe MigrateUpdateHeadPipelineForMergeRequestSidekiqQueue, :sidekiq, :redis
expect { described_class.new.down }.not_to raise_error
end
end
-
- def stubbed_worker(queue:)
- Class.new do
- include Sidekiq::Worker
- sidekiq_options queue: queue
- end
- end
end
diff --git a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb
index 99173708190..88aef3b70b4 100644
--- a/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb
+++ b/spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb
@@ -3,10 +3,10 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170324160416_migrate_user_activities_to_users_last_activity_on.rb')
-describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :delete do
+describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :migration do
let(:migration) { described_class.new }
- let!(:user_active_1) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
- let!(:user_active_2) { create(:user) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:user_active_1) { table(:users).create!(email: 'test1', username: 'test1') }
+ let!(:user_active_2) { table(:users).create!(email: 'test2', username: 'test2') }
def record_activity(user, time)
Gitlab::Redis::SharedState.with do |redis|
diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb
index 80468b9d01e..a0179ab3ceb 100644
--- a/spec/migrations/migrate_user_project_view_spec.rb
+++ b/spec/migrations/migrate_user_project_view_spec.rb
@@ -3,15 +3,15 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_project_view.rb')
-describe MigrateUserProjectView, :delete do
+describe MigrateUserProjectView, :migration do
let(:migration) { described_class.new }
- let!(:user) { create(:user, project_view: 'readme') } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ let!(:user) { table(:users).create!(project_view: User.project_views['readme']) }
describe '#up' do
it 'updates project view setting with new value' do
migration.up
- expect(user.reload.project_view).to eq('files')
+ expect(user.reload.project_view).to eq(User.project_views['files'])
end
end
end
diff --git a/spec/migrations/move_personal_snippets_files_spec.rb b/spec/migrations/move_personal_snippets_files_spec.rb
index 1f39ad98fb8..d94ae1e52f5 100644
--- a/spec/migrations/move_personal_snippets_files_spec.rb
+++ b/spec/migrations/move_personal_snippets_files_spec.rb
@@ -1,12 +1,19 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170612071012_move_personal_snippets_files.rb')
-describe MovePersonalSnippetsFiles do
+describe MovePersonalSnippetsFiles, :migration do
let(:migration) { described_class.new }
let(:test_dir) { File.join(Rails.root, "tmp", "tests", "move_snippet_files_test") }
let(:uploads_dir) { File.join(test_dir, 'uploads') }
let(:new_uploads_dir) { File.join(uploads_dir, '-', 'system') }
+ let(:notes) { table(:notes) }
+ let(:snippets) { table(:snippets) }
+ let(:uploads) { table(:uploads) }
+
+ let(:user) { table(:users).create!(email: 'user@example.com', projects_limit: 10) }
+ let(:project) { table(:projects).create!(name: 'gitlab', namespace_id: 1) }
+
before do
allow(CarrierWave).to receive(:root).and_return(test_dir)
allow(migration).to receive(:base_directory).and_return(test_dir)
@@ -16,14 +23,14 @@ describe MovePersonalSnippetsFiles do
describe "#up" do
let(:snippet) do
- snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id)
create_upload('picture.jpg', snippet)
snippet.update(description: markdown_linking_file('picture.jpg', snippet))
snippet
end
let(:snippet_with_missing_file) do
- snippet = create(:snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id, project_id: project.id)
create_upload('picture.jpg', snippet, create_file: false)
snippet.update(description: markdown_linking_file('picture.jpg', snippet))
snippet
@@ -62,7 +69,10 @@ describe MovePersonalSnippetsFiles do
secret = "secret#{snippet.id}"
file_location = "/uploads/-/system/personal_snippet/#{snippet.id}/#{secret}/picture.jpg"
markdown = markdown_linking_file('picture.jpg', snippet)
- note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ note = notes.create!(noteable_id: snippet.id,
+ noteable_type: Snippet,
+ note: "with #{markdown}",
+ author_id: user.id)
migration.up
@@ -73,14 +83,14 @@ describe MovePersonalSnippetsFiles do
describe "#down" do
let(:snippet) do
- snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id)
create_upload('picture.jpg', snippet, in_new_path: true)
snippet.update(description: markdown_linking_file('picture.jpg', snippet, in_new_path: true))
snippet
end
let(:snippet_with_missing_file) do
- snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id)
create_upload('picture.jpg', snippet, create_file: false, in_new_path: true)
snippet.update(description: markdown_linking_file('picture.jpg', snippet, in_new_path: true))
snippet
@@ -119,7 +129,10 @@ describe MovePersonalSnippetsFiles do
markdown = markdown_linking_file('picture.jpg', snippet, in_new_path: true)
secret = "secret#{snippet.id}"
file_location = "/uploads/personal_snippet/#{snippet.id}/#{secret}/picture.jpg"
- note = create(:note_on_personal_snippet, noteable: snippet, note: "with #{markdown}") # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ note = notes.create!(noteable_id: snippet.id,
+ noteable_type: Snippet,
+ note: "with #{markdown}",
+ author_id: user.id)
migration.down
@@ -135,7 +148,7 @@ describe MovePersonalSnippetsFiles do
secret = '123456789'
filename = 'hello.jpg'
- snippet = create(:personal_snippet) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ snippet = snippets.create!(author_id: user.id)
path_before = "/uploads/personal_snippet/#{snippet.id}/#{secret}/#{filename}"
path_after = "/uploads/system/personal_snippet/#{snippet.id}/#{secret}/#{filename}"
@@ -161,7 +174,11 @@ describe MovePersonalSnippetsFiles do
FileUtils.touch(absolute_path)
end
- create(:upload, model: snippet, path: "#{secret}/#{filename}", uploader: PersonalFileUploader) # rubocop:disable RSpec/FactoriesInMigrationSpecs
+ uploads.create!(model_id: snippet.id,
+ model_type: snippet.class,
+ path: "#{secret}/#{filename}",
+ uploader: PersonalFileUploader,
+ size: 100.kilobytes)
end
def markdown_linking_file(filename, snippet, in_new_path: false)
diff --git a/spec/migrations/remove_orphaned_label_links_spec.rb b/spec/migrations/remove_orphaned_label_links_spec.rb
index 13b8919343e..e8c44c141c3 100644
--- a/spec/migrations/remove_orphaned_label_links_spec.rb
+++ b/spec/migrations/remove_orphaned_label_links_spec.rb
@@ -10,6 +10,12 @@ describe RemoveOrphanedLabelLinks, :migration do
let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
let(:label) { create_label }
+ before do
+ # This migration was created before we introduced ProjectCiCdSetting#default_git_depth
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth).and_return(nil)
+ allow_any_instance_of(ProjectCiCdSetting).to receive(:default_git_depth=).and_return(0)
+ end
+
context 'add foreign key on label_id' do
let!(:label_link_with_label) { create_label_link(label_id: label.id) }
let!(:label_link_without_label) { create_label_link(label_id: nil) }
diff --git a/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb b/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
new file mode 100644
index 00000000000..54f3e264df0
--- /dev/null
+++ b/spec/migrations/schedule_fill_valid_time_for_pages_domain_certificates_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190524073827_schedule_fill_valid_time_for_pages_domain_certificates.rb')
+
+describe ScheduleFillValidTimeForPagesDomainCertificates, :migration, :sidekiq do
+ let(:migration_class) { described_class::MIGRATION }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ let(:domains_table) { table(:pages_domains) }
+
+ let(:certificate) do
+ File.read('spec/fixtures/passphrase_x509_certificate.crt')
+ end
+
+ before do
+ domains_table.create!(domain: "domain1.example.com", verification_code: "123")
+ domains_table.create!(domain: "domain2.example.com", verification_code: "123", certificate: '')
+ domains_table.create!(domain: "domain3.example.com", verification_code: "123", certificate: certificate)
+ domains_table.create!(domain: "domain4.example.com", verification_code: "123", certificate: certificate)
+ end
+
+ it 'correctly schedules background migrations' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ first_id = domains_table.find_by_domain("domain3.example.com").id
+ last_id = domains_table.find_by_domain("domain4.example.com").id
+
+ expect(migration_name).to be_scheduled_delayed_migration(5.minutes, first_id, last_id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ end
+ end
+ end
+
+ it 'sets certificate valid_not_before/not_after' do
+ perform_enqueued_jobs do
+ migrate!
+
+ domain = domains_table.find_by_domain("domain3.example.com")
+ expect(domain.certificate_valid_not_before)
+ .to eq(Time.parse("2018-03-23 14:02:08 UTC"))
+ expect(domain.certificate_valid_not_after)
+ .to eq(Time.parse("2019-03-23 14:02:08 UTC"))
+ end
+ end
+end
diff --git a/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb b/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb
new file mode 100644
index 00000000000..e397fbb7138
--- /dev/null
+++ b/spec/migrations/schedule_populate_merge_request_assignees_table_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190322132835_schedule_populate_merge_request_assignees_table.rb')
+
+describe SchedulePopulateMergeRequestAssigneesTable, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:namespace) { namespaces.create(name: 'gitlab', path: 'gitlab-org') }
+ let(:project) { projects.create(namespace_id: namespace.id, name: 'foo') }
+ let(:merge_requests) { table(:merge_requests) }
+
+ def create_merge_request(id)
+ params = {
+ id: id,
+ target_project_id: project.id,
+ target_branch: 'master',
+ source_project_id: project.id,
+ source_branch: 'mr name',
+ title: "mr name#{id}"
+ }
+
+ merge_requests.create!(params)
+ end
+
+ it 'correctly schedules background migrations' do
+ create_merge_request(1)
+ create_merge_request(2)
+ create_merge_request(3)
+
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(8.minutes, 1, 2)
+
+ expect(described_class::MIGRATION)
+ .to be_scheduled_delayed_migration(16.minutes, 3, 3)
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_runners_token_encryption_spec.rb b/spec/migrations/schedule_runners_token_encryption_spec.rb
index 376d2795277..97ff6c128f3 100644
--- a/spec/migrations/schedule_runners_token_encryption_spec.rb
+++ b/spec/migrations/schedule_runners_token_encryption_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20181121111200_schedule_runners_token_encryption')
-describe ScheduleRunnersTokenEncryption, :migration do
+describe ScheduleRunnersTokenEncryption, :migration, :sidekiq do
let(:settings) { table(:application_settings) }
let(:namespaces) { table(:namespaces) }
let(:projects) { table(:projects) }
diff --git a/spec/migrations/schedule_sync_issuables_state_id_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_spec.rb
new file mode 100644
index 00000000000..bc94f8820bd
--- /dev/null
+++ b/spec/migrations/schedule_sync_issuables_state_id_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190214112022_schedule_sync_issuables_state_id.rb')
+
+describe ScheduleSyncIssuablesStateId, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:issues) { table(:issues) }
+ let(:migration) { described_class.new }
+ let(:group) { namespaces.create!(name: 'gitlab', path: 'gitlab') }
+ let(:project) { projects.create!(namespace_id: group.id) }
+
+ shared_examples 'scheduling migrations' do
+ before do
+ Sidekiq::Worker.clear_all
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it 'correctly schedules issuable sync background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration).to be_scheduled_delayed_migration(120.seconds, resource_1.id, resource_2.id)
+ expect(migration).to be_scheduled_delayed_migration(240.seconds, resource_3.id, resource_4.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+ end
+
+ describe '#up' do
+ context 'issues' do
+ it 'migrates state column to integer' do
+ opened_issue = issues.create!(description: 'first', state: 'opened')
+ closed_issue = issues.create!(description: 'second', state: 'closed')
+ invalid_state_issue = issues.create!(description: 'fourth', state: 'not valid')
+
+ migrate!
+
+ expect(opened_issue.reload.state_id).to eq(Issue.available_states[:opened])
+ expect(closed_issue.reload.state_id).to eq(Issue.available_states[:closed])
+ expect(invalid_state_issue.reload.state_id).to be_nil
+ end
+
+ it_behaves_like 'scheduling migrations' do
+ let(:migration) { described_class::ISSUES_MIGRATION }
+ let!(:resource_1) { issues.create!(description: 'first', state: 'opened') }
+ let!(:resource_2) { issues.create!(description: 'second', state: 'closed') }
+ let!(:resource_3) { issues.create!(description: 'third', state: 'closed') }
+ let!(:resource_4) { issues.create!(description: 'fourth', state: 'closed') }
+ end
+ end
+
+ context 'merge requests' do
+ it 'migrates state column to integer' do
+ opened_merge_request = merge_requests.create!(state: 'opened', target_project_id: project.id, target_branch: 'feature1', source_branch: 'master')
+ closed_merge_request = merge_requests.create!(state: 'closed', target_project_id: project.id, target_branch: 'feature2', source_branch: 'master')
+ merged_merge_request = merge_requests.create!(state: 'merged', target_project_id: project.id, target_branch: 'feature3', source_branch: 'master')
+ locked_merge_request = merge_requests.create!(state: 'locked', target_project_id: project.id, target_branch: 'feature4', source_branch: 'master')
+
+ migrate!
+
+ expect(opened_merge_request.reload.state_id).to eq(MergeRequest.available_states[:opened])
+ expect(closed_merge_request.reload.state_id).to eq(MergeRequest.available_states[:closed])
+ expect(merged_merge_request.reload.state_id).to eq(MergeRequest.available_states[:merged])
+ expect(locked_merge_request.reload.state_id).to eq(MergeRequest.available_states[:locked])
+ end
+
+ it_behaves_like 'scheduling migrations' do
+ let(:migration) { described_class::MERGE_REQUESTS_MIGRATION }
+ let!(:resource_1) { merge_requests.create!(state: 'opened', target_project_id: project.id, target_branch: 'feature1', source_branch: 'master') }
+ let!(:resource_2) { merge_requests.create!(state: 'closed', target_project_id: project.id, target_branch: 'feature2', source_branch: 'master') }
+ let!(:resource_3) { merge_requests.create!(state: 'merged', target_project_id: project.id, target_branch: 'feature3', source_branch: 'master') }
+ let!(:resource_4) { merge_requests.create!(state: 'locked', target_project_id: project.id, target_branch: 'feature4', source_branch: 'master') }
+ end
+ end
+ end
+end
diff --git a/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb b/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb
new file mode 100644
index 00000000000..105c05bb7ca
--- /dev/null
+++ b/spec/migrations/schedule_sync_issuables_state_id_where_nil_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20190506135400_schedule_sync_issuables_state_id_where_nil')
+
+describe ScheduleSyncIssuablesStateIdWhereNil, :migration, :sidekiq do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:merge_requests) { table(:merge_requests) }
+ let(:issues) { table(:issues) }
+ let(:migration) { described_class.new }
+ let(:group) { namespaces.create!(name: 'gitlab', path: 'gitlab') }
+ let(:project) { projects.create!(namespace_id: group.id) }
+
+ shared_examples 'scheduling migrations' do
+ before do
+ Sidekiq::Worker.clear_all
+ stub_const("#{described_class.name}::BATCH_SIZE", 2)
+ end
+
+ it 'correctly schedules issuable sync background migration' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(migration).to be_scheduled_delayed_migration(120.seconds, resource_1.id, resource_3.id)
+ expect(migration).to be_scheduled_delayed_migration(240.seconds, resource_5.id, resource_5.id)
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ end
+ end
+ end
+ end
+
+ describe '#up' do
+ context 'issues' do
+ it_behaves_like 'scheduling migrations' do
+ let(:migration) { described_class::ISSUES_MIGRATION }
+ let!(:resource_1) { issues.create!(description: 'first', state: 'opened', state_id: nil) }
+ let!(:resource_2) { issues.create!(description: 'second', state: 'closed', state_id: 2) }
+ let!(:resource_3) { issues.create!(description: 'third', state: 'closed', state_id: nil) }
+ let!(:resource_4) { issues.create!(description: 'fourth', state: 'closed', state_id: 2) }
+ let!(:resource_5) { issues.create!(description: 'fifth', state: 'closed', state_id: nil) }
+ end
+ end
+
+ context 'merge requests' do
+ it_behaves_like 'scheduling migrations' do
+ let(:migration) { described_class::MERGE_REQUESTS_MIGRATION }
+ let!(:resource_1) { merge_requests.create!(state: 'opened', state_id: nil, target_project_id: project.id, target_branch: 'feature1', source_branch: 'master') }
+ let!(:resource_2) { merge_requests.create!(state: 'closed', state_id: 2, target_project_id: project.id, target_branch: 'feature2', source_branch: 'master') }
+ let!(:resource_3) { merge_requests.create!(state: 'merged', state_id: nil, target_project_id: project.id, target_branch: 'feature3', source_branch: 'master') }
+ let!(:resource_4) { merge_requests.create!(state: 'locked', state_id: 3, target_project_id: project.id, target_branch: 'feature4', source_branch: 'master') }
+ let!(:resource_5) { merge_requests.create!(state: 'locked', state_id: nil, target_project_id: project.id, target_branch: 'feature4', source_branch: 'master') }
+ end
+ end
+ end
+end
diff --git a/spec/migrations/truncate_user_fullname_spec.rb b/spec/migrations/truncate_user_fullname_spec.rb
new file mode 100644
index 00000000000..17fd4d9f688
--- /dev/null
+++ b/spec/migrations/truncate_user_fullname_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20190325080727_truncate_user_fullname.rb')
+
+describe TruncateUserFullname, :migration do
+ let(:users) { table(:users) }
+
+ let(:user_short) { create_user(name: 'abc', email: 'test_short@example.com') }
+ let(:user_long) { create_user(name: 'a' * 200 + 'z', email: 'test_long@example.com') }
+
+ def create_user(params)
+ users.create!(params.merge(projects_limit: 0))
+ end
+
+ it 'truncates user full name to the first 128 characters' do
+ expect { migrate! }.to change { user_long.reload.name }.to('a' * 128)
+ end
+
+ it 'does not truncate short names' do
+ expect { migrate! }.not_to change { user_short.reload.name.length }
+ end
+end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index eee80e9bad7..d9d60e02a97 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ability do
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index f49a61062c1..a5f8e999d5d 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe AbuseReport do
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index 129b2f92683..2762eaeccd3 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
@@ -7,7 +9,10 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end
end
- let(:session) { double(:session, id: '6919a6f1bb119dd7396fadc38fd18d0d') }
+ let(:session) do
+ double(:session, { id: '6919a6f1bb119dd7396fadc38fd18d0d',
+ '[]': {} })
+ end
let(:request) do
double(:request, {
@@ -83,6 +88,52 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end
end
+ describe '.list_sessions' do
+ it 'uses the ActiveSession lookup to return original sessions' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ _csrf_token: 'abcd' }))
+
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ %w[
+ 6919a6f1bb119dd7396fadc38fd18d0d
+ 59822c7d9fcdfa03725eff41782ad97d
+ ]
+ )
+ end
+
+ expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
+ end
+ end
+
+ describe '.session_ids_for_user' do
+ it 'uses the user lookup table to return session ids' do
+ session_ids = ['59822c7d9fcdfa03725eff41782ad97d']
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids)
+ end
+
+ expect(ActiveSession.session_ids_for_user(user)).to eq(session_ids)
+ end
+ end
+
+ describe '.sessions_from_ids' do
+ it 'uses the ActiveSession lookup to return original sessions' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ _csrf_token: 'abcd' }))
+ end
+
+ expect(ActiveSession.sessions_from_ids(['6919a6f1bb119dd7396fadc38fd18d0d'])).to eq [{ _csrf_token: 'abcd' }]
+ end
+
+ it 'avoids a redis lookup for an empty array' do
+ expect(Gitlab::Redis::SharedState).not_to receive(:with)
+
+ expect(ActiveSession.sessions_from_ids([])).to eq([])
+ end
+ end
+
describe '.set' do
it 'sets a new redis entry for the user session and a lookup entry' do
ActiveSession.set(user, request)
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 28d482adebf..209d138f956 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Appearance do
@@ -78,4 +80,22 @@ describe Appearance do
it { is_expected.to allow_value(hex).for(:message_font_color) }
it { is_expected.not_to allow_value('000').for(:message_font_color) }
end
+
+ describe 'email_header_and_footer_enabled' do
+ context 'default email_header_and_footer_enabled flag value' do
+ it 'returns email_header_and_footer_enabled as true' do
+ appearance = build(:appearance)
+
+ expect(appearance.email_header_and_footer_enabled?).to eq(false)
+ end
+ end
+
+ context 'when setting email_header_and_footer_enabled flag value' do
+ it 'returns email_header_and_footer_enabled as true' do
+ appearance = build(:appearance, email_header_and_footer_enabled: true)
+
+ expect(appearance.email_header_and_footer_enabled?).to eq(true)
+ end
+ end
+ end
end
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index fd25132ed3a..74573d0941c 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -11,6 +11,25 @@ describe ApplicationRecord do
end
end
+ describe '.safe_ensure_unique' do
+ let(:model) { build(:suggestion) }
+ let(:klass) { model.class }
+
+ before do
+ allow(model).to receive(:save).and_raise(ActiveRecord::RecordNotUnique)
+ end
+
+ it 'returns false when ActiveRecord::RecordNotUnique is raised' do
+ expect(model).to receive(:save).once
+ expect(klass.safe_ensure_unique { model.save }).to be_falsey
+ end
+
+ it 'retries based on retry count specified' do
+ expect(model).to receive(:save).exactly(3).times
+ expect(klass.safe_ensure_unique(retries: 2) { model.save }).to be_falsey
+ end
+ end
+
describe '.safe_find_or_create_by' do
it 'creates the user avoiding race conditions' do
expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
@@ -33,4 +52,10 @@ describe ApplicationRecord do
expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
end
end
+
+ describe '.underscore' do
+ it 'returns the underscored value of the class as a string' do
+ expect(MergeRequest.underscore).to eq('merge_request')
+ end
+ end
end
diff --git a/spec/models/application_setting/term_spec.rb b/spec/models/application_setting/term_spec.rb
index aa49594f4d1..dd263335b81 100644
--- a/spec/models/application_setting/term_spec.rb
+++ b/spec/models/application_setting/term_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ApplicationSetting::Term do
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 789e14e8a20..f8dc1541dd3 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ApplicationSetting do
- let(:setting) { described_class.create_from_defaults }
+ subject(:setting) { described_class.create_from_defaults }
it { include(CacheableAttributes) }
+ it { include(ApplicationSettingImplementation) }
it { expect(described_class.current_without_cache).to eq(described_class.last) }
it { expect(setting).to be_valid }
@@ -28,6 +31,20 @@ describe ApplicationSetting do
it { is_expected.to allow_value("dev.gitlab.com").for(:commit_email_hostname) }
it { is_expected.not_to allow_value("@dev.gitlab").for(:commit_email_hostname) }
+ it { is_expected.to allow_value("myemail@gitlab.com").for(:lets_encrypt_notification_email) }
+ it { is_expected.to allow_value(nil).for(:lets_encrypt_notification_email) }
+ it { is_expected.not_to allow_value("notanemail").for(:lets_encrypt_notification_email) }
+ it { is_expected.not_to allow_value("myemail@example.com").for(:lets_encrypt_notification_email) }
+ it { is_expected.to allow_value("myemail@test.example.com").for(:lets_encrypt_notification_email) }
+
+ context "when user accepted let's encrypt terms of service" do
+ before do
+ setting.update(lets_encrypt_terms_of_service_accepted: true)
+ end
+
+ it { is_expected.not_to allow_value(nil).for(:lets_encrypt_notification_email) }
+ end
+
describe 'default_artifacts_expire_in' do
it 'sets an error if it cannot parse' do
setting.update(default_artifacts_expire_in: 'a')
@@ -117,14 +134,6 @@ describe ApplicationSetting do
it { expect(setting.repository_storages).to eq(['default']) }
end
- context '#commit_email_hostname' do
- it 'returns configured gitlab hostname if commit_email_hostname is not defined' do
- setting.update(commit_email_hostname: nil)
-
- expect(setting.commit_email_hostname).to eq("users.noreply.#{Gitlab.config.gitlab.host}")
- end
- end
-
context 'auto_devops_domain setting' do
context 'when auto_devops_enabled? is true' do
before do
@@ -182,15 +191,6 @@ describe ApplicationSetting do
it { is_expected.not_to allow_value("").for(:repository_storages) }
it { is_expected.not_to allow_value(nil).for(:repository_storages) }
end
-
- describe '.pick_repository_storage' do
- it 'uses Array#sample to pick a random storage' do
- array = double('array', sample: 'random')
- expect(setting).to receive(:repository_storages).and_return(array)
-
- expect(setting.pick_repository_storage).to eq('random')
- end
- end
end
context 'housekeeping settings' do
@@ -298,15 +298,59 @@ describe ApplicationSetting do
expect(subject).to be_valid
end
end
+
+ describe 'when external authorization service is enabled' do
+ before do
+ setting.external_authorization_service_enabled = true
+ end
+
+ it { is_expected.not_to allow_value('not a URL').for(:external_authorization_service_url) }
+ it { is_expected.to allow_value('https://example.com').for(:external_authorization_service_url) }
+ it { is_expected.to allow_value('').for(:external_authorization_service_url) }
+ it { is_expected.not_to allow_value(nil).for(:external_authorization_service_default_label) }
+ it { is_expected.not_to allow_value(11).for(:external_authorization_service_timeout) }
+ it { is_expected.not_to allow_value(0).for(:external_authorization_service_timeout) }
+ it { is_expected.not_to allow_value('not a certificate').for(:external_auth_client_cert) }
+ it { is_expected.to allow_value('').for(:external_auth_client_cert) }
+ it { is_expected.to allow_value('').for(:external_auth_client_key) }
+
+ context 'when setting a valid client certificate for external authorization' do
+ let(:certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') }
+
+ before do
+ setting.external_auth_client_cert = certificate_data
+ end
+
+ it 'requires a valid client key when a certificate is set' do
+ expect(setting).not_to allow_value('fefefe').for(:external_auth_client_key)
+ end
+
+ it 'requires a matching certificate' do
+ other_private_key = File.read('spec/fixtures/x509_certificate_pk.key')
+
+ expect(setting).not_to allow_value(other_private_key).for(:external_auth_client_key)
+ end
+
+ it 'the credentials are valid when the private key can be read and matches the certificate' do
+ tls_attributes = [:external_auth_client_key_pass,
+ :external_auth_client_key,
+ :external_auth_client_cert]
+ setting.external_auth_client_key = File.read('spec/fixtures/passphrase_x509_certificate_pk.key')
+ setting.external_auth_client_key_pass = '5iveL!fe'
+
+ setting.validate
+
+ expect(setting.errors).not_to include(*tls_attributes)
+ end
+ end
+ end
end
context 'restrict creating duplicates' do
- before do
- described_class.create_from_defaults
- end
+ let!(:current_settings) { described_class.create_from_defaults }
- it 'raises an record creation violation if already created' do
- expect { described_class.create_from_defaults }.to raise_error(ActiveRecord::RecordNotUnique)
+ it 'returns the current settings' do
+ expect(described_class.create_from_defaults).to eq(current_settings)
end
end
@@ -367,65 +411,6 @@ describe ApplicationSetting do
end
end
- context 'restricted signup domains' do
- it 'sets single domain' do
- setting.domain_whitelist_raw = 'example.com'
- expect(setting.domain_whitelist).to eq(['example.com'])
- end
-
- it 'sets multiple domains with spaces' do
- setting.domain_whitelist_raw = 'example.com *.example.com'
- expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
- end
-
- it 'sets multiple domains with newlines and a space' do
- setting.domain_whitelist_raw = "example.com\n *.example.com"
- expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
- end
-
- it 'sets multiple domains with commas' do
- setting.domain_whitelist_raw = "example.com, *.example.com"
- expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
- end
- end
-
- context 'blacklisted signup domains' do
- it 'sets single domain' do
- setting.domain_blacklist_raw = 'example.com'
- expect(setting.domain_blacklist).to contain_exactly('example.com')
- end
-
- it 'sets multiple domains with spaces' do
- setting.domain_blacklist_raw = 'example.com *.example.com'
- expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
- end
-
- it 'sets multiple domains with newlines and a space' do
- setting.domain_blacklist_raw = "example.com\n *.example.com"
- expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
- end
-
- it 'sets multiple domains with commas' do
- setting.domain_blacklist_raw = "example.com, *.example.com"
- expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
- end
-
- it 'sets multiple domains with semicolon' do
- setting.domain_blacklist_raw = "example.com; *.example.com"
- expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
- end
-
- it 'sets multiple domains with mixture of everything' do
- setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com"
- expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com')
- end
-
- it 'sets multiple domain with file' do
- setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
- expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
- end
- end
-
describe 'performance bar settings' do
describe 'performance_bar_allowed_group' do
context 'with no performance_bar_allowed_group_id saved' do
@@ -462,142 +447,6 @@ describe ApplicationSetting do
end
end
- describe 'usage ping settings' do
- context 'when the usage ping is disabled in gitlab.yml' do
- before do
- allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false)
- end
-
- it 'does not allow the usage ping to be configured' do
- expect(setting.usage_ping_can_be_configured?).to be_falsey
- end
-
- context 'when the usage ping is disabled in the DB' do
- before do
- setting.usage_ping_enabled = false
- end
-
- it 'returns false for usage_ping_enabled' do
- expect(setting.usage_ping_enabled).to be_falsey
- end
- end
-
- context 'when the usage ping is enabled in the DB' do
- before do
- setting.usage_ping_enabled = true
- end
-
- it 'returns false for usage_ping_enabled' do
- expect(setting.usage_ping_enabled).to be_falsey
- end
- end
- end
-
- context 'when the usage ping is enabled in gitlab.yml' do
- before do
- allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true)
- end
-
- it 'allows the usage ping to be configured' do
- expect(setting.usage_ping_can_be_configured?).to be_truthy
- end
-
- context 'when the usage ping is disabled in the DB' do
- before do
- setting.usage_ping_enabled = false
- end
-
- it 'returns false for usage_ping_enabled' do
- expect(setting.usage_ping_enabled).to be_falsey
- end
- end
-
- context 'when the usage ping is enabled in the DB' do
- before do
- setting.usage_ping_enabled = true
- end
-
- it 'returns true for usage_ping_enabled' do
- expect(setting.usage_ping_enabled).to be_truthy
- end
- end
- end
- end
-
- describe '#allowed_key_types' do
- it 'includes all key types by default' do
- expect(setting.allowed_key_types).to contain_exactly(*described_class::SUPPORTED_KEY_TYPES)
- end
-
- it 'excludes disabled key types' do
- expect(setting.allowed_key_types).to include(:ed25519)
-
- setting.ed25519_key_restriction = described_class::FORBIDDEN_KEY_VALUE
-
- expect(setting.allowed_key_types).not_to include(:ed25519)
- end
- end
-
- describe '#key_restriction_for' do
- it 'returns the restriction value for recognised types' do
- setting.rsa_key_restriction = 1024
-
- expect(setting.key_restriction_for(:rsa)).to eq(1024)
- end
-
- it 'allows types to be passed as a string' do
- setting.rsa_key_restriction = 1024
-
- expect(setting.key_restriction_for('rsa')).to eq(1024)
- end
-
- it 'returns forbidden for unrecognised type' do
- expect(setting.key_restriction_for(:foo)).to eq(described_class::FORBIDDEN_KEY_VALUE)
- end
- end
-
- describe '#allow_signup?' do
- it 'returns true' do
- expect(setting.allow_signup?).to be_truthy
- end
-
- it 'returns false if signup is disabled' do
- allow(setting).to receive(:signup_enabled?).and_return(false)
-
- expect(setting.allow_signup?).to be_falsey
- end
-
- it 'returns false if password authentication is disabled for the web interface' do
- allow(setting).to receive(:password_authentication_enabled_for_web?).and_return(false)
-
- expect(setting.allow_signup?).to be_falsey
- end
- end
-
- describe '#user_default_internal_regex_enabled?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:user_default_external, :user_default_internal_regex, :result) do
- false | nil | false
- false | '' | false
- false | '^(?:(?!\.ext@).)*$\r?\n?' | false
- true | '' | false
- true | nil | false
- true | '^(?:(?!\.ext@).)*$\r?\n?' | true
- end
-
- with_them do
- before do
- setting.update(user_default_external: user_default_external)
- setting.update(user_default_internal_regex: user_default_internal_regex)
- end
-
- subject { setting.user_default_internal_regex_enabled? }
-
- it { is_expected.to eq(result) }
- end
- end
-
context 'diff limit settings' do
describe '#diff_max_patch_bytes' do
context 'validations' do
@@ -613,23 +462,5 @@ describe ApplicationSetting do
end
end
- describe '#archive_builds_older_than' do
- subject { setting.archive_builds_older_than }
-
- context 'when the archive_builds_in_seconds is set' do
- before do
- setting.archive_builds_in_seconds = 3600
- end
-
- it { is_expected.to be_within(1.minute).of(1.hour.ago) }
- end
-
- context 'when the archive_builds_in_seconds is set' do
- before do
- setting.archive_builds_in_seconds = nil
- end
-
- it { is_expected.to be_nil }
- end
- end
+ it_behaves_like 'application settings examples'
end
diff --git a/spec/models/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index 3f52091698c..8452ac69734 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AwardEmoji do
diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb
index 33dc19e3432..c661f5384ea 100644
--- a/spec/models/badge_spec.rb
+++ b/spec/models/badge_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Badge do
@@ -59,7 +61,7 @@ describe Badge do
end
shared_examples 'rendered_links' do
- it 'should use the project information to populate the url placeholders' do
+ it 'uses the project information to populate the url placeholders' do
stub_project_commit_info(project)
expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
diff --git a/spec/models/badges/group_badge_spec.rb b/spec/models/badges/group_badge_spec.rb
index ed7f83d0489..c297bc957ea 100644
--- a/spec/models/badges/group_badge_spec.rb
+++ b/spec/models/badges/group_badge_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupBadge do
diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb
index 0e1a8159cb6..d41c5cf2ca1 100644
--- a/spec/models/badges/project_badge_spec.rb
+++ b/spec/models/badges/project_badge_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectBadge do
@@ -12,7 +14,7 @@ describe ProjectBadge do
end
shared_examples 'rendered_links' do
- it 'should use the badge project information to populate the url placeholders' do
+ it 'uses the badge project information to populate the url placeholders' do
stub_project_commit_info(project)
expect(badge.public_send("rendered_#{method}")).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 05cf242e84d..8364293b908 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -1,4 +1,6 @@
# encoding: utf-8
+# frozen_string_literal: true
+
require 'rails_helper'
describe Blob do
@@ -41,6 +43,21 @@ describe Blob do
changelog.id
contributing.id
end
+
+ it 'does not include blobs from previous requests in later requests' do
+ changelog = described_class.lazy(project, commit_id, 'CHANGELOG')
+ contributing = described_class.lazy(same_project, commit_id, 'CONTRIBUTING.md')
+
+ # Access property so the values are loaded
+ changelog.id
+ contributing.id
+
+ readme = described_class.lazy(project, commit_id, 'README.md')
+
+ expect(project.repository).to receive(:blobs_at).with([[commit_id, 'README.md']]).once.and_call_original
+
+ readme.id
+ end
end
describe '#data' do
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
index 7ba28f72215..39c7a34f052 100644
--- a/spec/models/blob_viewer/base_spec.rb
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::Base do
diff --git a/spec/models/blob_viewer/changelog_spec.rb b/spec/models/blob_viewer/changelog_spec.rb
index db41eca0fc8..0fcc94182af 100644
--- a/spec/models/blob_viewer/changelog_spec.rb
+++ b/spec/models/blob_viewer/changelog_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::Changelog do
diff --git a/spec/models/blob_viewer/composer_json_spec.rb b/spec/models/blob_viewer/composer_json_spec.rb
index 85b0d9668a0..eda34779679 100644
--- a/spec/models/blob_viewer/composer_json_spec.rb
+++ b/spec/models/blob_viewer/composer_json_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::ComposerJson do
diff --git a/spec/models/blob_viewer/gemspec_spec.rb b/spec/models/blob_viewer/gemspec_spec.rb
index d8c4490637f..b6cc82c03ba 100644
--- a/spec/models/blob_viewer/gemspec_spec.rb
+++ b/spec/models/blob_viewer/gemspec_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::Gemspec do
diff --git a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
index 16bf947b493..db405ceb4f1 100644
--- a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
+++ b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::GitlabCiYml do
diff --git a/spec/models/blob_viewer/license_spec.rb b/spec/models/blob_viewer/license_spec.rb
index 222ed166ee0..e02bfae3829 100644
--- a/spec/models/blob_viewer/license_spec.rb
+++ b/spec/models/blob_viewer/license_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::License do
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
index fbaa8d47a71..b317278f3c8 100644
--- a/spec/models/blob_viewer/package_json_spec.rb
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::PackageJson do
diff --git a/spec/models/blob_viewer/podspec_json_spec.rb b/spec/models/blob_viewer/podspec_json_spec.rb
index 9a23877b23f..7f1fb8666fd 100644
--- a/spec/models/blob_viewer/podspec_json_spec.rb
+++ b/spec/models/blob_viewer/podspec_json_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::PodspecJson do
diff --git a/spec/models/blob_viewer/podspec_spec.rb b/spec/models/blob_viewer/podspec_spec.rb
index 02d06ea24d6..527ae79d766 100644
--- a/spec/models/blob_viewer/podspec_spec.rb
+++ b/spec/models/blob_viewer/podspec_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::Podspec do
diff --git a/spec/models/blob_viewer/readme_spec.rb b/spec/models/blob_viewer/readme_spec.rb
index 8d11d58cfca..958927bddb4 100644
--- a/spec/models/blob_viewer/readme_spec.rb
+++ b/spec/models/blob_viewer/readme_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::Readme do
diff --git a/spec/models/blob_viewer/route_map_spec.rb b/spec/models/blob_viewer/route_map_spec.rb
index c13662427b0..f7ce873c9d1 100644
--- a/spec/models/blob_viewer/route_map_spec.rb
+++ b/spec/models/blob_viewer/route_map_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::RouteMap do
diff --git a/spec/models/blob_viewer/server_side_spec.rb b/spec/models/blob_viewer/server_side_spec.rb
index 63790486200..f95305abe78 100644
--- a/spec/models/blob_viewer/server_side_spec.rb
+++ b/spec/models/blob_viewer/server_side_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BlobViewer::ServerSide do
diff --git a/spec/models/board_group_recent_visit_spec.rb b/spec/models/board_group_recent_visit_spec.rb
index 59ad4e5417e..558be61824f 100644
--- a/spec/models/board_group_recent_visit_spec.rb
+++ b/spec/models/board_group_recent_visit_spec.rb
@@ -50,15 +50,25 @@ describe BoardGroupRecentVisit do
end
describe '#latest' do
- it 'returns the most recent visited' do
- board2 = create(:board, group: group)
- board3 = create(:board, group: group)
+ def create_visit(time)
+ create :board_group_recent_visit, group: group, user: user, updated_at: time
+ end
- create :board_group_recent_visit, group: board.group, board: board, user: user, updated_at: 7.days.ago
- create :board_group_recent_visit, group: board2.group, board: board2, user: user, updated_at: 5.days.ago
- recent = create :board_group_recent_visit, group: board3.group, board: board3, user: user, updated_at: 1.day.ago
+ it 'returns the most recent visited' do
+ create_visit(7.days.ago)
+ create_visit(5.days.ago)
+ recent = create_visit(1.day.ago)
expect(described_class.latest(user, group)).to eq recent
end
+
+ it 'returns last 3 visited boards' do
+ create_visit(7.days.ago)
+ visit1 = create_visit(3.days.ago)
+ visit2 = create_visit(2.days.ago)
+ visit3 = create_visit(5.days.ago)
+
+ expect(described_class.latest(user, group, count: 3)).to eq([visit2, visit1, visit3])
+ end
end
end
diff --git a/spec/models/board_project_recent_visit_spec.rb b/spec/models/board_project_recent_visit_spec.rb
index 275581945fa..e404fb3bbdb 100644
--- a/spec/models/board_project_recent_visit_spec.rb
+++ b/spec/models/board_project_recent_visit_spec.rb
@@ -50,15 +50,25 @@ describe BoardProjectRecentVisit do
end
describe '#latest' do
- it 'returns the most recent visited' do
- board2 = create(:board, project: project)
- board3 = create(:board, project: project)
+ def create_visit(time)
+ create :board_project_recent_visit, project: project, user: user, updated_at: time
+ end
- create :board_project_recent_visit, project: board.project, board: board, user: user, updated_at: 7.days.ago
- create :board_project_recent_visit, project: board2.project, board: board2, user: user, updated_at: 5.days.ago
- recent = create :board_project_recent_visit, project: board3.project, board: board3, user: user, updated_at: 1.day.ago
+ it 'returns the most recent visited' do
+ create_visit(7.days.ago)
+ create_visit(5.days.ago)
+ recent = create_visit(1.day.ago)
expect(described_class.latest(user, project)).to eq recent
end
+
+ it 'returns last 3 visited boards' do
+ create_visit(7.days.ago)
+ visit1 = create_visit(3.days.ago)
+ visit2 = create_visit(2.days.ago)
+ visit3 = create_visit(5.days.ago)
+
+ expect(described_class.latest(user, project, count: 3)).to eq([visit2, visit1, visit3])
+ end
end
end
diff --git a/spec/models/board_spec.rb b/spec/models/board_spec.rb
index 12d29540137..54452faa0e1 100644
--- a/spec/models/board_spec.rb
+++ b/spec/models/board_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Board do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 89839709131..3ab013ddc0e 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BroadcastMessage do
@@ -95,6 +97,12 @@ describe BroadcastMessage do
end
end
+ describe '#attributes' do
+ it 'includes message_html field' do
+ expect(subject.attributes.keys).to include("cached_markdown_version", "message_html")
+ end
+ end
+
describe '#active?' do
it 'is truthy when started and not ended' do
message = build(:broadcast_message)
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
index 504bc710b25..82991937644 100644
--- a/spec/models/chat_name_spec.rb
+++ b/spec/models/chat_name_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatName do
diff --git a/spec/models/chat_team_spec.rb b/spec/models/chat_team_spec.rb
index 70a9a206faa..76beb3d506b 100644
--- a/spec/models/chat_team_spec.rb
+++ b/spec/models/chat_team_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatTeam do
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
index 0014bbcf9f5..f63816fd92a 100644
--- a/spec/models/ci/artifact_blob_spec.rb
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::ArtifactBlob do
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 741cdfef1a5..eb32198265b 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::Bridge do
@@ -8,6 +10,8 @@ describe Ci::Bridge do
create(:ci_bridge, pipeline: pipeline)
end
+ it { is_expected.to include_module(Ci::PipelineDelegator) }
+
describe '#tags' do
it 'only has a bridge tag' do
expect(bridge.tags).to eq [:bridge]
@@ -22,4 +26,19 @@ describe Ci::Bridge do
expect(status).to be_a Gitlab::Ci::Status::Success
end
end
+
+ describe '#scoped_variables_hash' do
+ it 'returns a hash representing variables' do
+ variables = %w[
+ CI_JOB_NAME CI_JOB_STAGE CI_COMMIT_SHA CI_COMMIT_SHORT_SHA
+ CI_COMMIT_BEFORE_SHA CI_COMMIT_REF_NAME CI_COMMIT_REF_SLUG
+ CI_PROJECT_ID CI_PROJECT_NAME CI_PROJECT_PATH
+ CI_PROJECT_PATH_SLUG CI_PROJECT_NAMESPACE CI_PIPELINE_IID
+ CI_CONFIG_PATH CI_PIPELINE_SOURCE CI_COMMIT_MESSAGE
+ CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION CI_COMMIT_REF_PROTECTED
+ ]
+
+ expect(bridge.scoped_variables_hash.keys).to include(*variables)
+ end
+ end
end
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 016a5899eef..917a65ddf21 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildMetadata do
diff --git a/spec/models/ci/build_runner_session_spec.rb b/spec/models/ci/build_runner_session_spec.rb
index 35622366829..e51fd009f50 100644
--- a/spec/models/ci/build_runner_session_spec.rb
+++ b/spec/models/ci/build_runner_session_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildRunnerSession, model: true do
@@ -11,25 +13,33 @@ describe Ci::BuildRunnerSession, model: true do
it { is_expected.to validate_presence_of(:url).with_message('must be a valid URL') }
describe '#terminal_specification' do
- let(:terminal_specification) { subject.terminal_specification }
+ let(:specification) { subject.terminal_specification }
+
+ it 'returns terminal.gitlab.com protocol' do
+ expect(specification[:subprotocols]).to eq ['terminal.gitlab.com']
+ end
+
+ it 'returns a wss url' do
+ expect(specification[:url]).to start_with('wss://')
+ end
it 'returns empty hash if no url' do
subject.url = ''
- expect(terminal_specification).to be_empty
+ expect(specification).to be_empty
end
context 'when url is present' do
it 'returns ca_pem nil if empty certificate' do
subject.certificate = ''
- expect(terminal_specification[:ca_pem]).to be_nil
+ expect(specification[:ca_pem]).to be_nil
end
it 'adds Authorization header if authorization is present' do
subject.authorization = 'whatever'
- expect(terminal_specification[:headers]).to include(Authorization: ['whatever'])
+ expect(specification[:headers]).to include(Authorization: ['whatever'])
end
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 17540443688..d98db024f73 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::Build do
@@ -23,9 +25,10 @@ describe Ci::Build do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to respond_to(:has_trace?) }
it { is_expected.to respond_to(:trace) }
- it { is_expected.to delegate_method(:merge_request?).to(:pipeline) }
-
- it { is_expected.to be_a(ArtifactMigratable) }
+ it { is_expected.to delegate_method(:merge_request_event?).to(:pipeline) }
+ it { is_expected.to delegate_method(:merge_request_ref?).to(:pipeline) }
+ it { is_expected.to delegate_method(:legacy_detached_merge_request_pipeline?).to(:pipeline) }
+ it { is_expected.to include_module(Ci::PipelineDelegator) }
describe 'associations' do
it 'has a bidirectional relationship with projects' do
@@ -107,14 +110,6 @@ describe Ci::Build do
end
end
- context 'when job has a legacy archive' do
- let!(:job) { create(:ci_build, :legacy_artifacts) }
-
- it 'returns the job' do
- is_expected.to include(job)
- end
- end
-
context 'when job has a job artifact archive' do
let!(:job) { create(:ci_build, :artifacts) }
@@ -152,8 +147,8 @@ describe Ci::Build do
end
end
- describe '.with_test_reports' do
- subject { described_class.with_test_reports }
+ describe '.with_reports' do
+ subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
context 'when build has a test report' do
let!(:build) { create(:ci_build, :success, :test_reports) }
@@ -186,6 +181,37 @@ describe Ci::Build do
end
end
+ describe '#enqueue' do
+ let(:build) { create(:ci_build, :created) }
+
+ subject { build.enqueue }
+
+ before do
+ allow(build).to receive(:any_unmet_prerequisites?).and_return(has_prerequisites)
+ allow(Ci::PrepareBuildService).to receive(:perform_async)
+ end
+
+ context 'build has unmet prerequisites' do
+ let(:has_prerequisites) { true }
+
+ it 'transitions to preparing' do
+ subject
+
+ expect(build).to be_preparing
+ end
+ end
+
+ context 'build has no prerequisites' do
+ let(:has_prerequisites) { false }
+
+ it 'transitions to pending' do
+ subject
+
+ expect(build).to be_pending
+ end
+ end
+ end
+
describe '#actionize' do
context 'when build is a created' do
before do
@@ -344,6 +370,18 @@ describe Ci::Build do
expect(build).to be_pending
end
+
+ context 'build has unmet prerequisites' do
+ before do
+ allow(build).to receive(:prerequisites).and_return([double])
+ end
+
+ it 'transits to preparing' do
+ subject
+
+ expect(build).to be_preparing
+ end
+ end
end
end
@@ -402,43 +440,11 @@ describe Ci::Build do
end
end
end
-
- context 'when legacy artifacts are used' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
-
- subject { build.artifacts? }
-
- context 'is expired' do
- let(:build) { create(:ci_build, :legacy_artifacts, :expired) }
-
- it { is_expected.to be_falsy }
- end
-
- context 'artifacts archive does not exist' do
- let(:build) { create(:ci_build) }
-
- it { is_expected.to be_falsy }
- end
-
- context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
-
- it { is_expected.to be_truthy }
- end
- end
end
describe '#browsable_artifacts?' do
subject { build.browsable_artifacts? }
- context 'artifacts metadata does not exist' do
- before do
- build.update(legacy_artifacts_metadata: nil)
- end
-
- it { is_expected.to be_falsy }
- end
-
context 'artifacts metadata does exists' do
let(:build) { create(:ci_build, :artifacts) }
@@ -694,12 +700,6 @@ describe Ci::Build do
it { is_expected.to be_truthy }
end
-
- context 'when build does not have job artifacts' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
-
- it { is_expected.to be_falsy }
- end
end
describe '#has_old_trace?' do
@@ -787,6 +787,10 @@ describe Ci::Build do
let(:deployment) { build.deployment }
let(:environment) { deployment.environment }
+ before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
+ end
+
it 'has deployments record with created status' do
expect(deployment).to be_created
expect(environment.name).to eq('review/master')
@@ -1022,11 +1026,11 @@ describe Ci::Build do
describe 'erasable build' do
shared_examples 'erasable' do
it 'removes artifact file' do
- expect(build.artifacts_file.exists?).to be_falsy
+ expect(build.artifacts_file.present?).to be_falsy
end
it 'removes artifact metadata file' do
- expect(build.artifacts_metadata.exists?).to be_falsy
+ expect(build.artifacts_metadata.present?).to be_falsy
end
it 'removes all job_artifacts' do
@@ -1118,7 +1122,7 @@ describe Ci::Build do
let!(:build) { create(:ci_build, :success, :artifacts) }
before do
- build.remove_artifacts_metadata!
+ build.erase_erasable_artifacts!
end
describe '#erase' do
@@ -1129,76 +1133,6 @@ describe Ci::Build do
end
end
end
-
- context 'old artifacts' do
- context 'build is erasable' do
- context 'new artifacts' do
- let!(:build) { create(:ci_build, :trace_artifact, :success, :legacy_artifacts) }
-
- describe '#erase' do
- before do
- build.erase(erased_by: erased_by)
- end
-
- context 'erased by user' do
- let!(:erased_by) { create(:user, username: 'eraser') }
-
- include_examples 'erasable'
-
- it 'records user who erased a build' do
- expect(build.erased_by).to eq erased_by
- end
- end
-
- context 'erased by system' do
- let(:erased_by) { nil }
-
- include_examples 'erasable'
-
- it 'does not set user who erased a build' do
- expect(build.erased_by).to be_nil
- end
- end
- end
-
- describe '#erasable?' do
- subject { build.erasable? }
- it { is_expected.to be_truthy }
- end
-
- describe '#erased?' do
- let!(:build) { create(:ci_build, :trace_artifact, :success, :legacy_artifacts) }
- subject { build.erased? }
-
- context 'job has not been erased' do
- it { is_expected.to be_falsey }
- end
-
- context 'job has been erased' do
- before do
- build.erase
- end
-
- it { is_expected.to be_truthy }
- end
- end
-
- context 'metadata and build trace are not available' do
- let!(:build) { create(:ci_build, :success, :legacy_artifacts) }
-
- before do
- build.remove_artifacts_metadata!
- end
-
- describe '#erase' do
- it 'does not raise error' do
- expect { build.erase }.not_to raise_error
- end
- end
- end
- end
- end
- end
end
describe '#erase_erasable_artifacts!' do
@@ -2054,54 +1988,6 @@ describe Ci::Build do
end
end
- context 'when updating the build' do
- let(:build) { create(:ci_build, artifacts_size: 23) }
-
- it 'updates project statistics' do
- build.artifacts_size = 42
-
- expect(build).to receive(:update_project_statistics_after_save).and_call_original
-
- expect { build.save! }
- .to change { build.project.statistics.reload.build_artifacts_size }
- .by(19)
- end
-
- context 'when the artifact size stays the same' do
- it 'does not update project statistics' do
- build.name = 'changed'
-
- expect(build).not_to receive(:update_project_statistics_after_save)
-
- build.save!
- end
- end
- end
-
- context 'when destroying the build' do
- let!(:build) { create(:ci_build, artifacts_size: 23) }
-
- it 'updates project statistics' do
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
-
- expect { build.destroy! }
- .to change { build.project.statistics.reload.build_artifacts_size }
- .by(-23)
- end
-
- context 'when the build is destroyed due to the project being destroyed' do
- it 'does not update the project statistics' do
- expect(ProjectStatistics)
- .not_to receive(:increment_statistic)
-
- build.project.update(pending_delete: true)
- build.project.destroy!
- end
- end
- end
-
describe '#variables' do
let(:container_registry_enabled) { false }
@@ -2114,55 +2000,56 @@ describe Ci::Build do
context 'returns variables' do
let(:predefined_variables) do
[
- { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true },
- { key: 'CI_PIPELINE_URL', value: project.web_url + "/pipelines/#{pipeline.id}", public: true },
- { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
- { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true },
- { key: 'CI_JOB_TOKEN', value: 'my-token', public: false },
- { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
- { key: 'CI_BUILD_TOKEN', value: 'my-token', public: false },
- { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
- { key: 'CI_REGISTRY_PASSWORD', value: 'my-token', public: false },
- { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
- { key: 'CI', value: 'true', public: true },
- { key: 'GITLAB_CI', value: 'true', public: true },
- { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true },
- { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
- { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
- { key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s, public: true },
- { key: 'CI_SERVER_VERSION_MINOR', value: Gitlab.version_info.minor.to_s, public: true },
- { key: 'CI_SERVER_VERSION_PATCH', value: Gitlab.version_info.patch.to_s, public: true },
- { key: 'CI_SERVER_REVISION', value: Gitlab.revision, public: true },
- { key: 'CI_JOB_NAME', value: 'test', public: true },
- { key: 'CI_JOB_STAGE', value: 'test', public: true },
- { key: 'CI_COMMIT_SHA', value: build.sha, public: true },
- { key: 'CI_COMMIT_SHORT_SHA', value: build.short_sha, public: true },
- { key: 'CI_COMMIT_BEFORE_SHA', value: build.before_sha, public: true },
- { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
- { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
- { key: 'CI_NODE_TOTAL', value: '1', public: true },
- { key: 'CI_BUILD_REF', value: build.sha, public: true },
- { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
- { key: 'CI_BUILD_REF_NAME', value: build.ref, public: true },
- { key: 'CI_BUILD_REF_SLUG', value: build.ref_slug, public: true },
- { key: 'CI_BUILD_NAME', value: 'test', public: true },
- { key: 'CI_BUILD_STAGE', value: 'test', public: true },
- { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true },
- { key: 'CI_PROJECT_NAME', value: project.path, public: true },
- { key: 'CI_PROJECT_PATH', value: project.full_path, public: true },
- { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true },
- { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true },
- { key: 'CI_PROJECT_URL', value: project.web_url, public: true },
- { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true },
- { key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true },
- { key: 'CI_PAGES_URL', value: project.pages_url, public: true },
- { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true },
- { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true },
- { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true },
- { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true },
- { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true },
- { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true },
- { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true }
+ { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true, masked: false },
+ { key: 'CI_PIPELINE_URL', value: project.web_url + "/pipelines/#{pipeline.id}", public: true, masked: false },
+ { key: 'CI_JOB_ID', value: build.id.to_s, public: true, masked: false },
+ { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true, masked: false },
+ { key: 'CI_JOB_TOKEN', value: 'my-token', public: false, masked: true },
+ { key: 'CI_BUILD_ID', value: build.id.to_s, public: true, masked: false },
+ { key: 'CI_BUILD_TOKEN', value: 'my-token', public: false, masked: true },
+ { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true, masked: false },
+ { key: 'CI_REGISTRY_PASSWORD', value: 'my-token', public: false, masked: true },
+ { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false, masked: false },
+ { key: 'CI', value: 'true', public: true, masked: false },
+ { key: 'GITLAB_CI', value: 'true', public: true, masked: false },
+ { key: 'GITLAB_FEATURES', value: project.licensed_features.join(','), public: true, masked: false },
+ { key: 'CI_SERVER_NAME', value: 'GitLab', public: true, masked: false },
+ { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true, masked: false },
+ { key: 'CI_SERVER_VERSION_MAJOR', value: Gitlab.version_info.major.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_VERSION_MINOR', value: Gitlab.version_info.minor.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_VERSION_PATCH', value: Gitlab.version_info.patch.to_s, public: true, masked: false },
+ { key: 'CI_SERVER_REVISION', value: Gitlab.revision, public: true, masked: false },
+ { key: 'CI_JOB_NAME', value: 'test', public: true, masked: false },
+ { key: 'CI_JOB_STAGE', value: 'test', public: true, masked: false },
+ { key: 'CI_COMMIT_SHA', value: build.sha, public: true, masked: false },
+ { key: 'CI_COMMIT_SHORT_SHA', value: build.short_sha, public: true, masked: false },
+ { key: 'CI_COMMIT_BEFORE_SHA', value: build.before_sha, public: true, masked: false },
+ { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true, masked: false },
+ { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true, masked: false },
+ { key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false },
+ { key: 'CI_BUILD_REF', value: build.sha, public: true, masked: false },
+ { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true, masked: false },
+ { key: 'CI_BUILD_REF_NAME', value: build.ref, public: true, masked: false },
+ { key: 'CI_BUILD_REF_SLUG', value: build.ref_slug, public: true, masked: false },
+ { key: 'CI_BUILD_NAME', value: 'test', public: true, masked: false },
+ { key: 'CI_BUILD_STAGE', value: 'test', public: true, masked: false },
+ { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true, masked: false },
+ { key: 'CI_PROJECT_NAME', value: project.path, public: true, masked: false },
+ { key: 'CI_PROJECT_PATH', value: project.full_path, public: true, masked: false },
+ { key: 'CI_PROJECT_PATH_SLUG', value: project.full_path_slug, public: true, masked: false },
+ { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.full_path, public: true, masked: false },
+ { key: 'CI_PROJECT_URL', value: project.web_url, public: true, masked: false },
+ { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true, masked: false },
+ { key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false },
+ { key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false },
+ { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false },
+ { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false },
+ { key: 'CI_CONFIG_PATH', value: pipeline.ci_yaml_file_path, public: true, masked: false },
+ { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false },
+ { key: 'CI_COMMIT_MESSAGE', value: pipeline.git_commit_message, public: true, masked: false },
+ { key: 'CI_COMMIT_TITLE', value: pipeline.git_commit_title, public: true, masked: false },
+ { key: 'CI_COMMIT_DESCRIPTION', value: pipeline.git_commit_description, public: true, masked: false },
+ { key: 'CI_COMMIT_REF_PROTECTED', value: (!!pipeline.protected_ref?).to_s, public: true, masked: false }
]
end
@@ -2175,10 +2062,10 @@ describe Ci::Build do
describe 'variables ordering' do
context 'when variables hierarchy is stubbed' do
- let(:build_pre_var) { { key: 'build', value: 'value', public: true } }
- let(:project_pre_var) { { key: 'project', value: 'value', public: true } }
- let(:pipeline_pre_var) { { key: 'pipeline', value: 'value', public: true } }
- let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true } }
+ let(:build_pre_var) { { key: 'build', value: 'value', public: true, masked: false } }
+ let(:project_pre_var) { { key: 'project', value: 'value', public: true, masked: false } }
+ let(:pipeline_pre_var) { { key: 'pipeline', value: 'value', public: true, masked: false } }
+ let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true, masked: false } }
before do
allow(build).to receive(:predefined_variables) { [build_pre_var] }
@@ -2200,7 +2087,7 @@ describe Ci::Build do
project_pre_var,
pipeline_pre_var,
build_yaml_var,
- { key: 'secret', value: 'value', public: false }])
+ { key: 'secret', value: 'value', public: false, masked: false }])
end
end
@@ -2233,10 +2120,10 @@ describe Ci::Build do
context 'when build has user' do
let(:user_variables) do
[
- { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true },
- { key: 'GITLAB_USER_EMAIL', value: user.email, public: true },
- { key: 'GITLAB_USER_LOGIN', value: user.username, public: true },
- { key: 'GITLAB_USER_NAME', value: user.name, public: true }
+ { key: 'GITLAB_USER_ID', value: user.id.to_s, public: true, masked: false },
+ { key: 'GITLAB_USER_EMAIL', value: user.email, public: true, masked: false },
+ { key: 'GITLAB_USER_LOGIN', value: user.username, public: true, masked: false },
+ { key: 'GITLAB_USER_NAME', value: user.name, public: true, masked: false }
]
end
@@ -2247,11 +2134,24 @@ describe Ci::Build do
it { user_variables.each { |v| is_expected.to include(v) } }
end
+ context 'when build belongs to a pipeline for merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline, source_branch: 'improve/awesome') }
+ let(:pipeline) { merge_request.all_pipelines.first }
+ let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) }
+
+ it 'returns values based on source ref' do
+ is_expected.to include(
+ { key: 'CI_COMMIT_REF_NAME', value: 'improve/awesome', public: true, masked: false },
+ { key: 'CI_COMMIT_REF_SLUG', value: 'improve-awesome', public: true, masked: false }
+ )
+ end
+ end
+
context 'when build has an environment' do
let(:environment_variables) do
[
- { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
- { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true }
+ { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true, masked: false },
+ { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true, masked: false }
]
end
@@ -2286,7 +2186,7 @@ describe Ci::Build do
before do
environment_variables <<
- { key: 'CI_ENVIRONMENT_URL', value: url, public: true }
+ { key: 'CI_ENVIRONMENT_URL', value: url, public: true, masked: false }
end
context 'when the URL was set from the job' do
@@ -2323,7 +2223,7 @@ describe Ci::Build do
end
let(:manual_variable) do
- { key: 'CI_JOB_MANUAL', value: 'true', public: true }
+ { key: 'CI_JOB_MANUAL', value: 'true', public: true, masked: false }
end
it { is_expected.to include(manual_variable) }
@@ -2331,7 +2231,7 @@ describe Ci::Build do
context 'when build is for tag' do
let(:tag_variable) do
- { key: 'CI_COMMIT_TAG', value: 'master', public: true }
+ { key: 'CI_COMMIT_TAG', value: 'master', public: true, masked: false }
end
before do
@@ -2343,7 +2243,7 @@ describe Ci::Build do
context 'when CI variable is defined' do
let(:ci_variable) do
- { key: 'SECRET_KEY', value: 'secret_value', public: false }
+ { key: 'SECRET_KEY', value: 'secret_value', public: false, masked: false }
end
before do
@@ -2358,7 +2258,7 @@ describe Ci::Build do
let(:ref) { Gitlab::Git::BRANCH_REF_PREFIX + build.ref }
let(:protected_variable) do
- { key: 'PROTECTED_KEY', value: 'protected_value', public: false }
+ { key: 'PROTECTED_KEY', value: 'protected_value', public: false, masked: false }
end
before do
@@ -2390,7 +2290,7 @@ describe Ci::Build do
context 'when group CI variable is defined' do
let(:ci_variable) do
- { key: 'SECRET_KEY', value: 'secret_value', public: false }
+ { key: 'SECRET_KEY', value: 'secret_value', public: false, masked: false }
end
before do
@@ -2405,7 +2305,7 @@ describe Ci::Build do
let(:ref) { Gitlab::Git::BRANCH_REF_PREFIX + build.ref }
let(:protected_variable) do
- { key: 'PROTECTED_KEY', value: 'protected_value', public: false }
+ { key: 'PROTECTED_KEY', value: 'protected_value', public: false, masked: false }
end
before do
@@ -2444,11 +2344,11 @@ describe Ci::Build do
let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
let(:user_trigger_variable) do
- { key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1', public: false }
+ { key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1', public: false, masked: false }
end
let(:predefined_trigger_variable) do
- { key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true }
+ { key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true, masked: false }
end
before do
@@ -2480,7 +2380,7 @@ describe Ci::Build do
context 'when pipeline has a variable' do
let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline) }
- it { is_expected.to include(pipeline_variable.to_runner_variable) }
+ it { is_expected.to include(key: pipeline_variable.key, value: pipeline_variable.value, public: false, masked: false) }
end
context 'when a job was triggered by a pipeline schedule' do
@@ -2497,16 +2397,16 @@ describe Ci::Build do
pipeline_schedule.reload
end
- it { is_expected.to include(pipeline_schedule_variable.to_runner_variable) }
+ it { is_expected.to include(key: pipeline_schedule_variable.key, value: pipeline_schedule_variable.value, public: false, masked: false) }
end
context 'when container registry is enabled' do
let(:container_registry_enabled) { true }
let(:ci_registry) do
- { key: 'CI_REGISTRY', value: 'registry.example.com', public: true }
+ { key: 'CI_REGISTRY', value: 'registry.example.com', public: true, masked: false }
end
let(:ci_registry_image) do
- { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true }
+ { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_url, public: true, masked: false }
end
context 'and is disabled for project' do
@@ -2535,13 +2435,13 @@ describe Ci::Build do
build.update(runner: runner)
end
- it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) }
- it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) }
- it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true, masked: false }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true, masked: false }) }
+ it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true, masked: false }) }
end
context 'when build is for a deployment' do
- let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
+ let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false, masked: false } }
before do
build.environment = 'production'
@@ -2555,7 +2455,7 @@ describe Ci::Build do
end
context 'when project has custom CI config path' do
- let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true } }
+ let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true, masked: false } }
before do
project.update(ci_config_path: 'custom')
@@ -2564,30 +2464,6 @@ describe Ci::Build do
it { is_expected.to include(ci_config_path) }
end
- context 'when using auto devops' do
- context 'and is enabled' do
- before do
- project.create_auto_devops!(enabled: true, domain: 'example.com')
- end
-
- it "includes AUTO_DEVOPS_DOMAIN" do
- is_expected.to include(
- { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true })
- end
- end
-
- context 'and is disabled' do
- before do
- project.create_auto_devops!(enabled: false, domain: 'example.com')
- end
-
- it "includes AUTO_DEVOPS_DOMAIN" do
- is_expected.not_to include(
- { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true })
- end
- end
- end
-
context 'when pipeline variable overrides build variable' do
before do
build.yaml_variables = [{ key: 'MYVAR', value: 'myvar', public: true }]
@@ -2598,9 +2474,9 @@ describe Ci::Build do
variables = subject.reverse.uniq { |variable| variable[:key] }.reverse
expect(variables)
- .not_to include(key: 'MYVAR', value: 'myvar', public: true)
+ .not_to include(key: 'MYVAR', value: 'myvar', public: true, masked: false)
expect(variables)
- .to include(key: 'MYVAR', value: 'pipeline value', public: false)
+ .to include(key: 'MYVAR', value: 'pipeline value', public: false, masked: false)
end
end
@@ -2616,13 +2492,13 @@ describe Ci::Build do
it 'includes CI_NODE_INDEX' do
is_expected.to include(
- { key: 'CI_NODE_INDEX', value: index.to_s, public: true }
+ { key: 'CI_NODE_INDEX', value: index.to_s, public: true, masked: false }
)
end
it 'includes correct CI_NODE_TOTAL' do
is_expected.to include(
- { key: 'CI_NODE_TOTAL', value: total.to_s, public: true }
+ { key: 'CI_NODE_TOTAL', value: total.to_s, public: true, masked: false }
)
end
end
@@ -2638,10 +2514,12 @@ describe Ci::Build do
)
end
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
+
it 'returns static predefined variables' do
expect(build.variables.size).to be >= 28
expect(build.variables)
- .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
+ .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false)
expect(build).not_to be_persisted
end
end
@@ -2651,8 +2529,8 @@ describe Ci::Build do
let(:deploy_token_variables) do
[
- { key: 'CI_DEPLOY_USER', value: deploy_token.username, public: true },
- { key: 'CI_DEPLOY_PASSWORD', value: deploy_token.token, public: false }
+ { key: 'CI_DEPLOY_USER', value: deploy_token.username, public: true, masked: false },
+ { key: 'CI_DEPLOY_PASSWORD', value: deploy_token.token, public: false, masked: true }
]
end
@@ -2661,13 +2539,13 @@ describe Ci::Build do
project.deploy_tokens << deploy_token
end
- it 'should include deploy token variables' do
+ it 'includes deploy token variables' do
is_expected.to include(*deploy_token_variables)
end
end
context 'when gitlab-deploy-token does not exist' do
- it 'should not include deploy token variables' do
+ it 'does not include deploy token variables' do
expect(subject.find { |v| v[:key] == 'CI_DEPLOY_USER'}).to be_nil
expect(subject.find { |v| v[:key] == 'CI_DEPLOY_PASSWORD'}).to be_nil
end
@@ -2687,6 +2565,8 @@ describe Ci::Build do
)
end
+ let(:pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
+
it 'does not persist the build' do
expect(build).to be_valid
expect(build).not_to be_persisted
@@ -2711,7 +2591,7 @@ describe Ci::Build do
end
expect(variables)
- .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
+ .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true, masked: false)
end
it 'does not return prohibited variables' do
@@ -2734,6 +2614,122 @@ describe Ci::Build do
end
end
+ describe '#secret_group_variables' do
+ subject { build.secret_group_variables }
+
+ let!(:variable) { create(:ci_group_variable, protected: true, group: group) }
+
+ context 'when ref is branch' do
+ let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
+
+ context 'when ref is protected' do
+ before do
+ create(:protected_branch, :developers_can_merge, name: 'master', project: project)
+ end
+
+ it { is_expected.to include(variable) }
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.not_to include(variable) }
+ end
+ end
+
+ context 'when ref is tag' do
+ let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, project: project) }
+
+ context 'when ref is protected' do
+ before do
+ create(:protected_tag, project: project, name: 'v*')
+ end
+
+ it { is_expected.to include(variable) }
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.not_to include(variable) }
+ end
+ end
+
+ context 'when ref is merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+ let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
+
+ context 'when ref is protected' do
+ before do
+ create(:protected_branch, :developers_can_merge, name: merge_request.source_branch, project: project)
+ end
+
+ it 'does not return protected variables as it is not supported for merge request pipelines' do
+ is_expected.not_to include(variable)
+ end
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.not_to include(variable) }
+ end
+ end
+ end
+
+ describe '#secret_project_variables' do
+ subject { build.secret_project_variables }
+
+ let!(:variable) { create(:ci_variable, protected: true, project: project) }
+
+ context 'when ref is branch' do
+ let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) }
+
+ context 'when ref is protected' do
+ before do
+ create(:protected_branch, :developers_can_merge, name: 'master', project: project)
+ end
+
+ it { is_expected.to include(variable) }
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.not_to include(variable) }
+ end
+ end
+
+ context 'when ref is tag' do
+ let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, project: project) }
+
+ context 'when ref is protected' do
+ before do
+ create(:protected_tag, project: project, name: 'v*')
+ end
+
+ it { is_expected.to include(variable) }
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.not_to include(variable) }
+ end
+ end
+
+ context 'when ref is merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+ let(:build) { create(:ci_build, ref: merge_request.source_branch, tag: false, pipeline: pipeline, project: project) }
+
+ context 'when ref is protected' do
+ before do
+ create(:protected_branch, :developers_can_merge, name: merge_request.source_branch, project: project)
+ end
+
+ it 'does not return protected variables as it is not supported for merge request pipelines' do
+ is_expected.not_to include(variable)
+ end
+ end
+
+ context 'when ref is not protected' do
+ it { is_expected.not_to include(variable) }
+ end
+ end
+ end
+
describe '#scoped_variables_hash' do
context 'when overriding CI variables' do
before do
@@ -2760,6 +2756,28 @@ describe Ci::Build do
end
end
+ describe '#any_unmet_prerequisites?' do
+ let(:build) { create(:ci_build, :created) }
+
+ subject { build.any_unmet_prerequisites? }
+
+ before do
+ allow(build).to receive(:prerequisites).and_return(prerequisites)
+ end
+
+ context 'build has prerequisites' do
+ let(:prerequisites) { [double] }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'build does not have prerequisites' do
+ let(:prerequisites) { [] }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
describe '#yaml_variables' do
let(:build) { create(:ci_build, pipeline: pipeline, yaml_variables: variables) }
@@ -2812,6 +2830,20 @@ describe Ci::Build do
end
end
+ describe 'state transition: any => [:preparing]' do
+ let(:build) { create(:ci_build, :created) }
+
+ before do
+ allow(build).to receive(:prerequisites).and_return([double])
+ end
+
+ it 'queues BuildPrepareWorker' do
+ expect(Ci::BuildPrepareWorker).to receive(:perform_async).with(build.id)
+
+ build.enqueue
+ end
+ end
+
describe 'state transition: any => [:pending]' do
let(:build) { create(:ci_build, :created) }
@@ -2991,7 +3023,7 @@ describe Ci::Build do
it 'does not try to create a todo' do
project.add_developer(user)
- expect(service).not_to receive(:commit_status_merge_requests)
+ expect(service).not_to receive(:pipeline_merge_requests)
subject.drop!
end
@@ -3027,7 +3059,23 @@ describe Ci::Build do
end
context 'when build is not configured to be retried' do
- subject { create(:ci_build, :running, project: project, user: user) }
+ subject { create(:ci_build, :running, project: project, user: user, pipeline: pipeline) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let(:merge_request) do
+ create(:merge_request, :opened,
+ source_branch: 'feature',
+ source_project: project,
+ target_branch: 'master',
+ target_project: project)
+ end
it 'does not retry build' do
expect(described_class).not_to receive(:retry)
@@ -3046,7 +3094,10 @@ describe Ci::Build do
it 'creates a todo' do
project.add_developer(user)
- expect(service).to receive(:commit_status_merge_requests)
+ expect_next_instance_of(TodoService) do |todo_service|
+ expect(todo_service)
+ .to receive(:merge_request_build_failed).with(merge_request)
+ end
subject.drop!
end
@@ -3299,6 +3350,18 @@ describe Ci::Build do
end
end
+ describe '#report_artifacts' do
+ subject { build.report_artifacts }
+
+ context 'when the build has reports' do
+ let!(:report) { create(:ci_job_artifact, :codequality, job: build) }
+
+ it 'returns the artifacts with reports' do
+ expect(subject).to contain_exactly(report)
+ end
+ end
+ end
+
describe '#artifacts_metadata_entry' do
set(:build) { create(:ci_build, project: project) }
let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
@@ -3423,6 +3486,24 @@ describe Ci::Build do
it { is_expected.to be_falsey }
end
end
+
+ context 'when refspecs feature is required by build' do
+ before do
+ allow(build).to receive(:merge_request_ref?) { true }
+ end
+
+ context 'when runner provides given feature' do
+ let(:runner_features) { { refspecs: true } }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when runner does not provide given feature' do
+ let(:runner_features) { {} }
+
+ it { is_expected.to be_falsey }
+ end
+ end
end
describe '#deployment_status' do
diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb
index d214fdf369a..59db347582b 100644
--- a/spec/models/ci/build_trace_chunk_spec.rb
+++ b/spec/models/ci/build_trace_chunk_spec.rb
@@ -171,7 +171,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
shared_examples_for 'Scheduling sidekiq worker to flush data to persist store' do
- context 'when new data fullfilled chunk size' do
+ context 'when new data fulfilled chunk size' do
let(:new_data) { 'a' * described_class::CHUNK_SIZE }
it 'schedules trace chunk flush worker' do
@@ -193,7 +193,7 @@ describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state do
end
shared_examples_for 'Scheduling no sidekiq worker' do
- context 'when new data fullfilled chunk size' do
+ context 'when new data fulfilled chunk size' do
let(:new_data) { 'a' * described_class::CHUNK_SIZE }
it 'does not schedule trace chunk flush worker' do
diff --git a/spec/models/ci/build_trace_chunks/database_spec.rb b/spec/models/ci/build_trace_chunks/database_spec.rb
index d8fc9d57e95..eb94d7dae38 100644
--- a/spec/models/ci/build_trace_chunks/database_spec.rb
+++ b/spec/models/ci/build_trace_chunks/database_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildTraceChunks::Database do
diff --git a/spec/models/ci/build_trace_chunks/fog_spec.rb b/spec/models/ci/build_trace_chunks/fog_spec.rb
index 8f49190af13..b8d78bcd069 100644
--- a/spec/models/ci/build_trace_chunks/fog_spec.rb
+++ b/spec/models/ci/build_trace_chunks/fog_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildTraceChunks::Fog do
diff --git a/spec/models/ci/build_trace_chunks/redis_spec.rb b/spec/models/ci/build_trace_chunks/redis_spec.rb
index 9da1e6a95ee..6cff33d24fa 100644
--- a/spec/models/ci/build_trace_chunks/redis_spec.rb
+++ b/spec/models/ci/build_trace_chunks/redis_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildTraceChunks::Redis, :clean_gitlab_redis_shared_state do
diff --git a/spec/models/ci/build_trace_section_name_spec.rb b/spec/models/ci/build_trace_section_name_spec.rb
index 386ee6880cb..11e2d27ff79 100644
--- a/spec/models/ci/build_trace_section_name_spec.rb
+++ b/spec/models/ci/build_trace_section_name_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildTraceSectionName, model: true do
diff --git a/spec/models/ci/build_trace_section_spec.rb b/spec/models/ci/build_trace_section_spec.rb
index 541a9a36fb8..5bd3a953ec0 100644
--- a/spec/models/ci/build_trace_section_spec.rb
+++ b/spec/models/ci/build_trace_section_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildTraceSection, model: true do
diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb
index 838fa63cb1f..36c65d92840 100644
--- a/spec/models/ci/group_spec.rb
+++ b/spec/models/ci/group_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::Group do
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index 1b10501701c..406a69f3bbc 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -1,10 +1,14 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::GroupVariable do
subject { build(:ci_group_variable) }
- it { is_expected.to include_module(HasVariable) }
+ it_behaves_like "CI variable"
+
it { is_expected.to include_module(Presentable) }
+ it { is_expected.to include_module(Maskable) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id).with_message(/\(\w+\) has already been taken/) }
describe '.unprotected' do
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index c68ba02b8de..1ba66565e03 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::JobArtifact do
@@ -17,6 +19,25 @@ describe Ci::JobArtifact do
it_behaves_like 'having unique enum values'
+ it_behaves_like 'UpdateProjectStatistics' do
+ subject { build(:ci_job_artifact, :archive, size: 106365) }
+ end
+
+ describe '.with_reports' do
+ let!(:artifact) { create(:ci_job_artifact, :archive) }
+
+ subject { described_class.with_reports }
+
+ it { is_expected.to be_empty }
+
+ context 'when there are reports' do
+ let!(:metrics_report) { create(:ci_job_artifact, :junit) }
+ let!(:codequality_report) { create(:ci_job_artifact, :codequality) }
+
+ it { is_expected.to eq([metrics_report, codequality_report]) }
+ end
+ end
+
describe '.test_reports' do
subject { described_class.test_reports }
@@ -100,12 +121,6 @@ describe Ci::JobArtifact do
it 'sets the size from the file size' do
expect(artifact.size).to eq(106365)
end
-
- it 'updates the project statistics' do
- expect { artifact }
- .to change { project.statistics.reload.build_artifacts_size }
- .by(106365)
- end
end
context 'updating the artifact file' do
@@ -113,12 +128,6 @@ describe Ci::JobArtifact do
artifact.update!(file: fixture_file_upload('spec/fixtures/dk.png'))
expect(artifact.size).to eq(1062)
end
-
- it 'updates the project statistics' do
- expect { artifact.update!(file: fixture_file_upload('spec/fixtures/dk.png')) }
- .to change { artifact.project.statistics.reload.build_artifacts_size }
- .by(1062 - 106365)
- end
end
describe 'validates file format' do
@@ -257,34 +266,6 @@ describe Ci::JobArtifact do
end
end
- context 'when destroying the artifact' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
- let!(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
-
- it 'updates the project statistics' do
- artifact = build.job_artifacts.first
-
- expect(ProjectStatistics)
- .to receive(:increment_statistic)
- .and_call_original
-
- expect { artifact.destroy }
- .to change { project.statistics.reload.build_artifacts_size }
- .by(-106365)
- end
-
- context 'when it is destroyed from the project level' do
- it 'does not update the project statistics' do
- expect(ProjectStatistics)
- .not_to receive(:increment_statistic)
-
- project.update(pending_delete: true)
- project.destroy!
- end
- end
- end
-
describe 'file is being stored' do
subject { create(:ci_job_artifact, :archive) }
diff --git a/spec/models/ci/legacy_stage_spec.rb b/spec/models/ci/legacy_stage_spec.rb
index 0c33c1466b7..bb812cc0533 100644
--- a/spec/models/ci/legacy_stage_spec.rb
+++ b/spec/models/ci/legacy_stage_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::LegacyStage do
@@ -270,4 +272,6 @@ describe Ci::LegacyStage do
def create_job(type, status: 'success', stage: stage_name, **opts)
create(type, pipeline: pipeline, stage: stage, status: status, **opts)
end
+
+ it_behaves_like 'manual playable stage', :ci_stage
end
diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb
index 8ee15f0e734..227870eb27f 100644
--- a/spec/models/ci/pipeline_schedule_spec.rb
+++ b/spec/models/ci/pipeline_schedule_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::PipelineSchedule do
+ subject { build(:ci_pipeline_schedule) }
+
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:owner) }
@@ -33,34 +37,127 @@ describe Ci::PipelineSchedule do
expect(pipeline_schedule).not_to be_valid
end
end
+
+ context 'when cron contains trailing whitespaces' do
+ it 'strips the attribute' do
+ pipeline_schedule = build(:ci_pipeline_schedule, cron: ' 0 0 * * * ')
+
+ expect(pipeline_schedule).to be_valid
+ expect(pipeline_schedule.cron).to eq('0 0 * * *')
+ end
+ end
+ end
+
+ describe '.runnable_schedules' do
+ subject { described_class.runnable_schedules }
+
+ let!(:pipeline_schedule) do
+ Timecop.freeze(1.day.ago) do
+ create(:ci_pipeline_schedule, :hourly)
+ end
+ end
+
+ it 'returns the runnable schedule' do
+ is_expected.to eq([pipeline_schedule])
+ end
+
+ context 'when there are no runnable schedules' do
+ let!(:pipeline_schedule) { }
+
+ it 'returns an empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.preloaded' do
+ subject { described_class.preloaded }
+
+ before do
+ create_list(:ci_pipeline_schedule, 3)
+ end
+
+ it 'preloads the associations' do
+ subject
+
+ query = ActiveRecord::QueryRecorder.new { subject.each(&:project) }
+
+ expect(query.count).to eq(2)
+ end
end
describe '#set_next_run_at' do
- let!(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly) }
+ let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_at) }
+
+ let(:expected_next_run_at) do
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(ideal_next_run_at)
+ end
+
+ let(:cron_worker_next_run_at) do
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(Time.zone.now)
+ end
context 'when creates new pipeline schedule' do
- let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone)
- .next_time_from(Time.now)
+ it 'updates next_run_at automatically' do
+ expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at)
+ end
+ end
+
+ context 'when PipelineScheduleWorker runs at a specific interval' do
+ before do
+ allow(Settings).to receive(:cron_jobs) do
+ {
+ 'pipeline_schedule_worker' => {
+ 'cron' => '0 1 2 3 *'
+ }
+ }
+ end
end
- it 'updates next_run_at automatically' do
- expect(described_class.last.next_run_at).to eq(expected_next_run_at)
+ it "updates next_run_at to the sidekiq worker's execution time" do
+ expect(pipeline_schedule.next_run_at.min).to eq(0)
+ expect(pipeline_schedule.next_run_at.hour).to eq(1)
+ expect(pipeline_schedule.next_run_at.day).to eq(2)
+ expect(pipeline_schedule.next_run_at.month).to eq(3)
end
end
- context 'when updates cron of exsisted pipeline schedule' do
- let(:new_cron) { '0 0 1 1 *' }
+ context 'when pipeline schedule runs every minute' do
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :every_minute) }
- let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(new_cron, pipeline_schedule.cron_timezone)
- .next_time_from(Time.now)
+ it "updates next_run_at to the sidekiq worker's execution time", :quarantine do
+ expect(pipeline_schedule.next_run_at).to eq(cron_worker_next_run_at)
end
+ end
+
+ context 'when there are two different pipeline schedules in different time zones' do
+ let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'Eastern Time (US & Canada)') }
+ let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+
+ it 'sets different next_run_at' do
+ expect(pipeline_schedule_1.next_run_at).not_to eq(pipeline_schedule_2.next_run_at)
+ end
+ end
+
+ context 'when there are two different pipeline schedules in the same time zones' do
+ let(:pipeline_schedule_1) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+ let(:pipeline_schedule_2) { create(:ci_pipeline_schedule, :weekly, cron_timezone: 'UTC') }
+
+ it 'sets the sames next_run_at' do
+ expect(pipeline_schedule_1.next_run_at).to eq(pipeline_schedule_2.next_run_at)
+ end
+ end
+
+ context 'when updates cron of exsisted pipeline schedule' do
+ let(:new_cron) { '0 0 1 1 *' }
it 'updates next_run_at automatically' do
pipeline_schedule.update!(cron: new_cron)
- expect(described_class.last.next_run_at).to eq(expected_next_run_at)
+ expect(pipeline_schedule.next_run_at).to eq(expected_next_run_at)
end
end
end
@@ -70,10 +167,11 @@ describe Ci::PipelineSchedule do
context 'when reschedules after 10 days from now' do
let(:future_time) { 10.days.from_now }
+ let(:ideal_next_run_at) { pipeline_schedule.send(:ideal_next_run_at) }
let(:expected_next_run_at) do
- Gitlab::Ci::CronParser.new(pipeline_schedule.cron, pipeline_schedule.cron_timezone)
- .next_time_from(future_time)
+ Gitlab::Ci::CronParser.new(Settings.cron_jobs['pipeline_schedule_worker']['cron'], Time.zone.name)
+ .next_time_from(ideal_next_run_at)
end
it 'points to proper next_run_at' do
@@ -86,38 +184,6 @@ describe Ci::PipelineSchedule do
end
end
- describe '#real_next_run' do
- subject do
- described_class.last.real_next_run(worker_cron: worker_cron,
- worker_time_zone: worker_time_zone)
- end
-
- context 'when GitLab time_zone is UTC' do
- before do
- allow(Time).to receive(:zone)
- .and_return(ActiveSupport::TimeZone[worker_time_zone])
- end
-
- let(:worker_time_zone) { 'UTC' }
-
- context 'when cron_timezone is Eastern Time (US & Canada)' do
- before do
- create(:ci_pipeline_schedule, :nightly,
- cron_timezone: 'Eastern Time (US & Canada)')
- end
-
- let(:worker_cron) { '0 1 2 3 *' }
-
- it 'returns the next time worker executes' do
- expect(subject.min).to eq(0)
- expect(subject.hour).to eq(1)
- expect(subject.day).to eq(2)
- expect(subject.month).to eq(3)
- end
- end
- end
- end
-
describe '#job_variables' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule) }
diff --git a/spec/models/ci/pipeline_schedule_variable_spec.rb b/spec/models/ci/pipeline_schedule_variable_spec.rb
index dc8427f28bc..c96a24d5042 100644
--- a/spec/models/ci/pipeline_schedule_variable_spec.rb
+++ b/spec/models/ci/pipeline_schedule_variable_spec.rb
@@ -1,7 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::PipelineScheduleVariable do
subject { build(:ci_pipeline_schedule_variable) }
- it { is_expected.to include_module(HasVariable) }
+ it_behaves_like "CI variable"
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b9567ab4d65..a8701f0efa4 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::Pipeline, :mailer do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
set(:project) { create(:project) }
@@ -78,11 +82,11 @@ describe Ci::Pipeline, :mailer do
context 'when merge request pipelines exist' do
let!(:merge_request_pipeline_1) do
- create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
end
let!(:merge_request_pipeline_2) do
- create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
end
let(:merge_request) do
@@ -104,11 +108,11 @@ describe Ci::Pipeline, :mailer do
let!(:branch_pipeline_2) { create(:ci_pipeline, source: :push) }
let!(:merge_request_pipeline_1) do
- create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
end
let!(:merge_request_pipeline_2) do
- create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
end
let(:merge_request) do
@@ -128,11 +132,373 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '.merge_request' do
- subject { described_class.merge_request }
+ describe '.for_sha' do
+ subject { described_class.for_sha(sha) }
+
+ let(:sha) { 'abc' }
+ let!(:pipeline) { create(:ci_pipeline, sha: 'abc') }
+
+ it 'returns the pipeline' do
+ is_expected.to contain_exactly(pipeline)
+ end
+
+ context 'when argument is array' do
+ let(:sha) { %w[abc def] }
+ let!(:pipeline_2) { create(:ci_pipeline, sha: 'def') }
+
+ it 'returns the pipelines' do
+ is_expected.to contain_exactly(pipeline, pipeline_2)
+ end
+ end
+
+ context 'when sha is empty' do
+ let(:sha) { nil }
+
+ it 'does not return anything' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.for_source_sha' do
+ subject { described_class.for_source_sha(source_sha) }
+
+ let(:source_sha) { 'abc' }
+ let!(:pipeline) { create(:ci_pipeline, source_sha: 'abc') }
+
+ it 'returns the pipeline' do
+ is_expected.to contain_exactly(pipeline)
+ end
+
+ context 'when argument is array' do
+ let(:source_sha) { %w[abc def] }
+ let!(:pipeline_2) { create(:ci_pipeline, source_sha: 'def') }
+
+ it 'returns the pipelines' do
+ is_expected.to contain_exactly(pipeline, pipeline_2)
+ end
+ end
+
+ context 'when source_sha is empty' do
+ let(:source_sha) { nil }
+
+ it 'does not return anything' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.for_sha_or_source_sha' do
+ subject { described_class.for_sha_or_source_sha(sha) }
+
+ let(:sha) { 'abc' }
+
+ context 'when sha is matched' do
+ let!(:pipeline) { create(:ci_pipeline, sha: sha) }
+
+ it 'returns the pipeline' do
+ is_expected.to contain_exactly(pipeline)
+ end
+ end
+
+ context 'when source sha is matched' do
+ let!(:pipeline) { create(:ci_pipeline, source_sha: sha) }
+
+ it 'returns the pipeline' do
+ is_expected.to contain_exactly(pipeline)
+ end
+ end
+
+ context 'when both sha and source sha are not matched' do
+ let!(:pipeline) { create(:ci_pipeline, sha: 'bcd', source_sha: 'bcd') }
+
+ it 'does not return anything' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.detached_merge_request_pipelines' do
+ subject { described_class.detached_merge_request_pipelines(merge_request, sha) }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, sha: merge_request.diff_head_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:sha) { merge_request.diff_head_sha }
+
+ it 'returns detached merge request pipelines' do
+ is_expected.to eq([pipeline])
+ end
+
+ context 'when sha does not exist' do
+ let(:sha) { 'abc' }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, source_sha: merge_request.diff_head_sha)
+ end
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '#detached_merge_request_pipeline?' do
+ subject { pipeline.detached_merge_request_pipeline? }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, target_sha: target_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_sha) { nil }
+
+ it { is_expected.to be_truthy }
+
+ context 'when target sha exists' do
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '.merge_request_pipelines' do
+ subject { described_class.merge_request_pipelines(merge_request, source_sha) }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, source_sha: merge_request.diff_head_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:source_sha) { merge_request.diff_head_sha }
+
+ it 'returns merge pipelines' do
+ is_expected.to eq([pipeline])
+ end
+
+ context 'when source sha is empty' do
+ let(:source_sha) { nil }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is detached merge request pipeline' do
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, sha: merge_request.diff_head_sha)
+ end
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '#merge_request_pipeline?' do
+ subject { pipeline.merge_request_pipeline? }
+
+ let!(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, target_sha: target_sha)
+ end
+
+ let(:merge_request) { create(:merge_request) }
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it { is_expected.to be_truthy }
+
+ context 'when target sha is empty' do
+ let(:target_sha) { nil }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#merge_request_ref?' do
+ subject { pipeline.merge_request_ref? }
+
+ it 'calls MergeRequest#merge_request_ref?' do
+ expect(MergeRequest).to receive(:merge_request_ref?).with(pipeline.ref)
+
+ subject
+ end
+ end
+
+ describe '#legacy_detached_merge_request_pipeline?' do
+ subject { pipeline.legacy_detached_merge_request_pipeline? }
+
+ set(:merge_request) { create(:merge_request) }
+ let(:ref) { 'feature' }
+ let(:target_sha) { nil }
+
+ let(:pipeline) do
+ build(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: ref, target_sha: target_sha)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when pipeline ref is a merge request ref' do
+ let(:ref) { 'refs/merge-requests/1/head' }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when target sha is set' do
+ let(:target_sha) { 'target-sha' }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#matches_sha_or_source_sha?' do
+ subject { pipeline.matches_sha_or_source_sha?(sample_sha) }
+
+ let(:sample_sha) { Digest::SHA1.hexdigest(SecureRandom.hex) }
+
+ context 'when sha matches' do
+ let(:pipeline) { build(:ci_pipeline, sha: sample_sha) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when source_sha matches' do
+ let(:pipeline) { build(:ci_pipeline, source_sha: sample_sha) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when both sha and source_sha do not matche' do
+ let(:pipeline) { build(:ci_pipeline, sha: 'test', source_sha: 'test') }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ describe '#source_ref' do
+ subject { pipeline.source_ref }
+
+ let(:pipeline) { create(:ci_pipeline, ref: 'feature') }
+
+ it 'returns source ref' do
+ is_expected.to eq('feature')
+ end
+
+ context 'when the pipeline is a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path)
+ end
+
+ it 'returns source ref' do
+ is_expected.to eq(merge_request.source_branch)
+ end
+ end
+ end
+
+ describe '#source_ref_slug' do
+ subject { pipeline.source_ref_slug }
+
+ let(:pipeline) { create(:ci_pipeline, ref: 'feature') }
+
+ it 'slugifies with the source ref' do
+ expect(Gitlab::Utils).to receive(:slugify).with('feature')
+
+ subject
+ end
+
+ context 'when the pipeline is a detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, ref: merge_request.ref_path)
+ end
+
+ it 'slugifies with the source ref of the merge request' do
+ expect(Gitlab::Utils).to receive(:slugify).with(merge_request.source_branch)
+
+ subject
+ end
+ end
+ end
+
+ describe '.triggered_for_branch' do
+ subject { described_class.triggered_for_branch(ref) }
+
+ let(:project) { create(:project, :repository) }
+ let(:ref) { 'feature' }
+ let!(:pipeline) { create(:ci_pipeline, ref: ref) }
+
+ it 'returns the pipeline' do
+ is_expected.to eq([pipeline])
+ end
+
+ context 'when sha is not specified' do
+ it 'returns the pipeline' do
+ expect(described_class.triggered_for_branch(ref)).to eq([pipeline])
+ end
+ end
+
+ context 'when pipeline is triggered for tag' do
+ let(:ref) { 'v1.1.0' }
+ let!(:pipeline) { create(:ci_pipeline, ref: ref, tag: true) }
+
+ it 'does not return the pipeline' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is triggered for merge_request' do
+ let!(:merge_request) do
+ create(:merge_request,
+ :with_merge_request_pipeline,
+ source_project: project,
+ source_branch: ref,
+ target_project: project,
+ target_branch: 'master')
+ end
+
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+
+ it 'does not return the pipeline' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.with_reports' do
+ subject { described_class.with_reports(Ci::JobArtifact.test_reports) }
+
+ context 'when pipeline has a test report' do
+ let!(:pipeline_with_report) { create(:ci_pipeline, :with_test_reports) }
+
+ it 'selects the pipeline' do
+ is_expected.to eq([pipeline_with_report])
+ end
+ end
+
+ context 'when pipeline does not have metrics reports' do
+ let!(:pipeline_without_report) { create(:ci_empty_pipeline) }
+
+ it 'does not select the pipeline' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.merge_request_event' do
+ subject { described_class.merge_request_event }
context 'when there is a merge request pipeline' do
- let!(:pipeline) { create(:ci_pipeline, source: :merge_request, merge_request: merge_request) }
+ let!(:pipeline) { create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request) }
let(:merge_request) { create(:merge_request) }
it 'returns merge request pipeline first' do
@@ -153,7 +519,7 @@ describe Ci::Pipeline, :mailer do
let(:pipeline) { build(:ci_pipeline, source: source, merge_request: merge_request) }
context 'when source is merge request' do
- let(:source) { :merge_request }
+ let(:source) { :merge_request_event }
context 'when merge request is specified' do
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_project: project, target_branch: 'master') }
@@ -372,12 +738,13 @@ describe Ci::Pipeline, :mailer do
CI_PIPELINE_SOURCE
CI_COMMIT_MESSAGE
CI_COMMIT_TITLE
- CI_COMMIT_DESCRIPTION]
+ CI_COMMIT_DESCRIPTION
+ CI_COMMIT_REF_PROTECTED]
end
context 'when source is merge request' do
let(:pipeline) do
- create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
end
let(:merge_request) do
@@ -385,9 +752,16 @@ describe Ci::Pipeline, :mailer do
source_project: project,
source_branch: 'feature',
target_project: project,
- target_branch: 'master')
+ target_branch: 'master',
+ assignees: assignees,
+ milestone: milestone,
+ labels: labels)
end
+ let(:assignees) { create_list(:user, 2) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:labels) { create_list(:label, 2) }
+
it 'exposes merge request pipeline variables' do
expect(subject.to_hash)
.to include(
@@ -398,10 +772,16 @@ describe Ci::Pipeline, :mailer do
'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
+ 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => pipeline.target_sha.to_s,
'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s)
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
+ 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => pipeline.source_sha.to_s,
+ 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
+ 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
+ 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
+ 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).join(','))
end
context 'when source project does not exist' do
@@ -417,6 +797,30 @@ describe Ci::Pipeline, :mailer do
CI_MERGE_REQUEST_SOURCE_BRANCH_NAME])
end
end
+
+ context 'without assignee' do
+ let(:assignees) { [] }
+
+ it 'does not expose assignee variable' do
+ expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
+ end
+ end
+
+ context 'without milestone' do
+ let(:milestone) { nil }
+
+ it 'does not expose milestone variable' do
+ expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE')
+ end
+ end
+
+ context 'without labels' do
+ let(:labels) { [] }
+
+ it 'does not expose labels variable' do
+ expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS')
+ end
+ end
end
end
@@ -886,16 +1290,28 @@ describe Ci::Pipeline, :mailer do
end
describe '#started_at' do
- it 'updates on transitioning to running' do
- build.run
+ let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }
+
+ %i[created preparing pending].each do |status|
+ context "from #{status}" do
+ let(:from_status) { status }
+
+ it 'updates on transitioning to running' do
+ pipeline.run
- expect(pipeline.reload.started_at).not_to be_nil
+ expect(pipeline.started_at).not_to be_nil
+ end
+ end
end
- it 'does not update on transitioning to success' do
- build.success
+ context 'from created' do
+ let(:from_status) { :created }
+
+ it 'does not update on transitioning to success' do
+ pipeline.succeed
- expect(pipeline.reload.started_at).to be_nil
+ expect(pipeline.started_at).to be_nil
+ end
end
end
@@ -914,27 +1330,49 @@ describe Ci::Pipeline, :mailer do
end
describe 'merge request metrics' do
- let(:project) { create(:project, :repository) }
- let(:pipeline) { FactoryBot.create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: project.repository.commit('master').id) }
- let!(:merge_request) { create(:merge_request, source_project: project, source_branch: pipeline.ref) }
+ let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }
before do
expect(PipelineMetricsWorker).to receive(:perform_async).with(pipeline.id)
end
context 'when transitioning to running' do
- it 'schedules metrics workers' do
- pipeline.run
+ %i[created preparing pending].each do |status|
+ context "from #{status}" do
+ let(:from_status) { status }
+
+ it 'schedules metrics workers' do
+ pipeline.run
+ end
+ end
end
end
context 'when transitioning to success' do
+ let(:from_status) { 'created' }
+
it 'schedules metrics workers' do
pipeline.succeed
end
end
end
+ describe 'merge on success' do
+ let(:pipeline) { create(:ci_empty_pipeline, status: from_status) }
+
+ %i[created preparing pending running].each do |status|
+ context "from #{status}" do
+ let(:from_status) { status }
+
+ it 'schedules pipeline success worker' do
+ expect(PipelineSuccessWorker).to receive(:perform_async).with(pipeline.id)
+
+ pipeline.succeed
+ end
+ end
+ end
+ end
+
describe 'pipeline caching' do
it 'performs ExpirePipelinesCacheWorker' do
expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id)
@@ -943,6 +1381,40 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe 'auto merge' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, :running, project: merge_request.source_project,
+ ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha)
+ end
+
+ before do
+ merge_request.update_head_pipeline
+ end
+
+ %w[succeed! drop! cancel! skip!].each do |action|
+ context "when the pipeline recieved #{action} event" do
+ it 'performs AutoMergeProcessWorker' do
+ expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request.id)
+
+ pipeline.public_send(action)
+ end
+ end
+ end
+
+ context 'when auto merge is not enabled in the merge request' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'performs AutoMergeProcessWorker' do
+ expect(AutoMergeProcessWorker).not_to receive(:perform_async)
+
+ pipeline.succeed!
+ end
+ end
+ end
+
def create_build(name, *traits, queued_at: current, started_from: 0, **opts)
create(:ci_build, *traits,
name: name,
@@ -967,7 +1439,7 @@ describe Ci::Pipeline, :mailer do
context 'when source is merge request' do
let(:pipeline) do
- create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
end
let(:merge_request) do
@@ -1017,7 +1489,7 @@ describe Ci::Pipeline, :mailer do
context 'when ref is merge request' do
let(:pipeline) do
create(:ci_pipeline,
- source: :merge_request,
+ source: :merge_request_event,
merge_request: merge_request)
end
@@ -1087,6 +1559,14 @@ describe Ci::Pipeline, :mailer do
end
end
+ context 'with a branch name as the ref' do
+ it 'looks up commit with the full ref name' do
+ expect(pipeline.project).to receive(:commit).with('refs/heads/master').and_call_original
+
+ expect(pipeline).to be_latest
+ end
+ end
+
context 'with not latest sha' do
before do
pipeline.update(
@@ -1180,7 +1660,7 @@ describe Ci::Pipeline, :mailer do
context 'when source is merge request' do
let(:pipeline) do
- create(:ci_pipeline, source: :merge_request, merge_request: merge_request)
+ create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request)
end
let(:merge_request) do
@@ -1453,6 +1933,18 @@ describe Ci::Pipeline, :mailer do
subject { pipeline.reload.status }
+ context 'on prepare' do
+ before do
+ # Prevent skipping directly to 'pending'
+ allow(build).to receive(:prerequisites).and_return([double])
+ allow(Ci::BuildPrepareWorker).to receive(:perform_async)
+
+ build.enqueue
+ end
+
+ it { is_expected.to eq('preparing') }
+ end
+
context 'on queuing' do
before do
build.enqueue
@@ -2087,7 +2579,7 @@ describe Ci::Pipeline, :mailer do
end
end
- describe "#merge_requests" do
+ describe "#merge_requests_as_head_pipeline" do
let(:project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') }
@@ -2095,85 +2587,100 @@ describe Ci::Pipeline, :mailer do
allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { 'a288a022a53a5a944fae87bcec6efc87b7061808' }
merge_request = create(:merge_request, source_project: project, head_pipeline: pipeline, source_branch: pipeline.ref)
- expect(pipeline.merge_requests).to eq([merge_request])
+ expect(pipeline.merge_requests_as_head_pipeline).to eq([merge_request])
end
it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do
create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
- expect(pipeline.merge_requests).to be_empty
+ expect(pipeline.merge_requests_as_head_pipeline).to be_empty
end
it "doesn't return merge requests whose `diff_head_sha` doesn't match the pipeline's SHA" do
create(:merge_request, source_project: project, source_branch: pipeline.ref)
allow_any_instance_of(MergeRequest).to receive(:diff_head_sha) { '97de212e80737a608d939f648d959671fb0a0142b' }
- expect(pipeline.merge_requests).to be_empty
+ expect(pipeline.merge_requests_as_head_pipeline).to be_empty
end
end
describe "#all_merge_requests" do
let(:project) { create(:project) }
- let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master') }
- it "returns all merge requests having the same source branch" do
- merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ shared_examples 'a method that returns all merge requests for a given pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: pipeline_project, ref: 'master') }
- expect(pipeline.all_merge_requests).to eq([merge_request])
- end
-
- it "doesn't return merge requests having a different source branch" do
- create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master')
-
- expect(pipeline.all_merge_requests).to be_empty
- end
+ it "returns all merge requests having the same source branch" do
+ merge_request = create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: pipeline.ref)
- context 'when there is a merge request pipeline' do
- let(:source_branch) { 'feature' }
- let(:target_branch) { 'master' }
-
- let!(:pipeline) do
- create(:ci_pipeline,
- source: :merge_request,
- project: project,
- ref: source_branch,
- merge_request: merge_request)
+ expect(pipeline.all_merge_requests).to eq([merge_request])
end
- let(:merge_request) do
- create(:merge_request,
- source_project: project,
- source_branch: source_branch,
- target_project: project,
- target_branch: target_branch)
- end
+ it "doesn't return merge requests having a different source branch" do
+ create(:merge_request, source_project: pipeline_project, target_project: project, source_branch: 'feature', target_branch: 'master')
- it 'returns an associated merge request' do
- expect(pipeline.all_merge_requests).to eq([merge_request])
+ expect(pipeline.all_merge_requests).to be_empty
end
- context 'when there is another merge request pipeline that targets a different branch' do
- let(:target_branch_2) { 'merge-test' }
+ context 'when there is a merge request pipeline' do
+ let(:source_branch) { 'feature' }
+ let(:target_branch) { 'master' }
- let!(:pipeline_2) do
+ let!(:pipeline) do
create(:ci_pipeline,
- source: :merge_request,
- project: project,
+ source: :merge_request_event,
+ project: pipeline_project,
ref: source_branch,
- merge_request: merge_request_2)
+ merge_request: merge_request)
end
- let(:merge_request_2) do
+ let(:merge_request) do
create(:merge_request,
- source_project: project,
+ source_project: pipeline_project,
source_branch: source_branch,
target_project: project,
- target_branch: target_branch_2)
+ target_branch: target_branch)
end
- it 'does not return an associated merge request' do
- expect(pipeline.all_merge_requests).not_to include(merge_request_2)
+ it 'returns an associated merge request' do
+ expect(pipeline.all_merge_requests).to eq([merge_request])
end
+
+ context 'when there is another merge request pipeline that targets a different branch' do
+ let(:target_branch_2) { 'merge-test' }
+
+ let!(:pipeline_2) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ project: pipeline_project,
+ ref: source_branch,
+ merge_request: merge_request_2)
+ end
+
+ let(:merge_request_2) do
+ create(:merge_request,
+ source_project: pipeline_project,
+ source_branch: source_branch,
+ target_project: project,
+ target_branch: target_branch_2)
+ end
+
+ it 'does not return an associated merge request' do
+ expect(pipeline.all_merge_requests).not_to include(merge_request_2)
+ end
+ end
+ end
+ end
+
+ it_behaves_like 'a method that returns all merge requests for a given pipeline' do
+ let(:pipeline_project) { project }
+ end
+
+ context 'for a fork' do
+ let(:fork) { fork_project(project) }
+
+ it_behaves_like 'a method that returns all merge requests for a given pipeline' do
+ let(:pipeline_project) { fork }
end
end
end
@@ -2301,18 +2808,19 @@ describe Ci::Pipeline, :mailer do
end
describe '#latest_builds_with_artifacts' do
- let!(:pipeline) { create(:ci_pipeline, :success) }
-
- let!(:build) do
- create(:ci_build, :success, :artifacts, pipeline: pipeline)
- end
+ let!(:fresh_build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+ let!(:stale_build) { create(:ci_build, :success, :expired, :artifacts, pipeline: pipeline) }
it 'returns an Array' do
expect(pipeline.latest_builds_with_artifacts).to be_an_instance_of(Array)
end
- it 'returns the latest builds' do
- expect(pipeline.latest_builds_with_artifacts).to eq([build])
+ it 'returns the latest builds with non-expired artifacts' do
+ expect(pipeline.latest_builds_with_artifacts).to contain_exactly(fresh_build)
+ end
+
+ it 'does not return builds with expired artifacts' do
+ expect(pipeline.latest_builds_with_artifacts).not_to include(stale_build)
end
it 'memoizes the returned relation' do
@@ -2324,8 +2832,8 @@ describe Ci::Pipeline, :mailer do
end
end
- describe '#has_test_reports?' do
- subject { pipeline.has_test_reports? }
+ describe '#has_reports?' do
+ subject { pipeline.has_reports?(Ci::JobArtifact.test_reports) }
context 'when pipeline has builds with test reports' do
before do
@@ -2468,4 +2976,36 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ describe '#find_stage_by_name' do
+ let(:pipeline) { create(:ci_pipeline) }
+ let(:stage_name) { 'test' }
+
+ let(:stage) do
+ create(:ci_stage_entity,
+ pipeline: pipeline,
+ project: pipeline.project,
+ name: 'test')
+ end
+
+ before do
+ create_list(:ci_build, 2, pipeline: pipeline, stage: stage.name)
+ end
+
+ subject { pipeline.find_stage_by_name!(stage_name) }
+
+ context 'when stage exists' do
+ it { is_expected.to eq(stage) }
+ end
+
+ context 'when stage does not exist' do
+ let(:stage_name) { 'build' }
+
+ it 'raises an ActiveRecord exception' do
+ expect do
+ subject
+ end.to raise_exception(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_variable_spec.rb b/spec/models/ci/pipeline_variable_spec.rb
index 03d09cb31d6..e8c7ce088e2 100644
--- a/spec/models/ci/pipeline_variable_spec.rb
+++ b/spec/models/ci/pipeline_variable_spec.rb
@@ -1,9 +1,12 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::PipelineVariable do
subject { build(:ci_pipeline_variable) }
- it { is_expected.to include_module(HasVariable) }
+ it_behaves_like "CI variable"
+
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:pipeline_id) }
describe '#hook_attrs' do
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index eb2daed7f32..f735a89f69f 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::Runner do
@@ -70,10 +72,9 @@ describe Ci::Runner do
expect(instance_runner.errors.full_messages).to include('Runner cannot have projects assigned')
end
- it 'should fail to save a group assigned to a project runner even if the runner is already saved' do
- group_runner
-
- expect { create(:group, runners: [project_runner]) }
+ it 'fails to save a group assigned to a project runner even if the runner is already saved' do
+ group.runners << project_runner
+ expect { group.save! }
.to raise_error(ActiveRecord::RecordInvalid)
end
end
diff --git a/spec/models/ci/stage_spec.rb b/spec/models/ci/stage_spec.rb
index 3228c400155..85cd32fb03a 100644
--- a/spec/models/ci/stage_spec.rb
+++ b/spec/models/ci/stage_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::Stage, :models do
@@ -283,4 +285,6 @@ describe Ci::Stage, :models do
end
end
end
+
+ it_behaves_like 'manual playable stage', :ci_stage_entity
end
diff --git a/spec/models/ci/trigger_request_spec.rb b/spec/models/ci/trigger_request_spec.rb
index 7dcf3528f73..d04349bec92 100644
--- a/spec/models/ci/trigger_request_spec.rb
+++ b/spec/models/ci/trigger_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::TriggerRequest do
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index d4b72205203..fde8375f2a5 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::Trigger do
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 875e8b2b682..a231c7eaed8 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -1,11 +1,15 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::Variable do
subject { build(:ci_variable) }
+ it_behaves_like "CI variable"
+
describe 'validations' do
- it { is_expected.to include_module(HasVariable) }
it { is_expected.to include_module(Presentable) }
+ it { is_expected.to include_module(Maskable) }
it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) }
end
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
index cf5cbf8ec5c..8d853a04e33 100644
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ b/spec/models/clusters/applications/cert_manager_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Clusters::Applications::CertManager do
@@ -8,13 +10,39 @@ describe Clusters::Applications::CertManager do
include_examples 'cluster application version specs', :clusters_applications_cert_managers
include_examples 'cluster application initial status specs'
+ describe '#can_uninstall?' do
+ subject { cert_manager.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#install_command' do
- let(:cluster_issuer_file) { { "cluster_issuer.yaml": "---\napiVersion: certmanager.k8s.io/v1alpha1\nkind: ClusterIssuer\nmetadata:\n name: letsencrypt-prod\nspec:\n acme:\n server: https://acme-v02.api.letsencrypt.org/directory\n email: admin@example.com\n privateKeySecretRef:\n name: letsencrypt-prod\n http01: {}\n" } }
+ let(:cert_email) { 'admin@example.com' }
+
+ let(:cluster_issuer_file) do
+ file_contents = <<~EOF
+ ---
+ apiVersion: certmanager.k8s.io/v1alpha1
+ kind: ClusterIssuer
+ metadata:
+ name: letsencrypt-prod
+ spec:
+ acme:
+ server: https://acme-v02.api.letsencrypt.org/directory
+ email: #{cert_email}
+ privateKeySecretRef:
+ name: letsencrypt-prod
+ http01: {}
+ EOF
+
+ { "cluster_issuer.yaml": file_contents }
+ end
+
subject { cert_manager.install_command }
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with cert_manager arguments' do
+ it 'is initialized with cert_manager arguments' do
expect(subject.name).to eq('certmanager')
expect(subject.chart).to eq('stable/cert-manager')
expect(subject.version).to eq('v0.5.2')
@@ -24,12 +52,13 @@ describe Clusters::Applications::CertManager do
end
context 'for a specific user' do
+ let(:cert_email) { 'abc@xyz.com' }
+
before do
- cert_manager.email = 'abc@xyz.com'
- cluster_issuer_file[:'cluster_issuer.yaml'].gsub! 'admin@example.com', 'abc@xyz.com'
+ cert_manager.email = cert_email
end
- it 'should use his/her email to register issuer with certificate provider' do
+ it 'uses his/her email to register issuer with certificate provider' do
expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file))
end
end
@@ -45,7 +74,7 @@ describe Clusters::Applications::CertManager do
context 'application failed to install previously' do
let(:cert_manager) { create(:clusters_applications_cert_managers, :errored, version: '0.0.1') }
- it 'should be initialized with the locked version' do
+ it 'is initialized with the locked version' do
expect(subject.version).to eq('v0.5.2')
end
end
@@ -57,7 +86,7 @@ describe Clusters::Applications::CertManager do
subject { application.files }
- it 'should include cert_manager specific keys in the values.yaml file' do
+ it 'includes cert_manager specific keys in the values.yaml file' do
expect(values).to include('ingressShim')
end
end
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index f16eff92167..6ea6c110d62 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Clusters::Applications::Helm do
@@ -16,6 +18,14 @@ describe Clusters::Applications::Helm do
it { is_expected.to contain_exactly(installed_cluster, updated_cluster) }
end
+ describe '#can_uninstall?' do
+ let(:helm) { create(:clusters_applications_helm) }
+
+ subject { helm.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#issue_client_cert' do
let(:application) { create(:clusters_applications_helm) }
subject { application.issue_client_cert }
@@ -34,11 +44,11 @@ describe Clusters::Applications::Helm do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InitCommand) }
- it 'should be initialized with 1 arguments' do
+ it 'is initialized with 1 arguments' do
expect(subject.name).to eq('helm')
end
- it 'should have cert files' do
+ it 'has cert files' do
expect(subject.files[:'ca.pem']).to be_present
expect(subject.files[:'ca.pem']).to eq(helm.ca_cert)
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index d5fd42509a3..292ddabd2d8 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Clusters::Applications::Ingress do
@@ -16,6 +18,12 @@ describe Clusters::Applications::Ingress do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
end
+ describe '#can_uninstall?' do
+ subject { ingress.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#make_installed!' do
before do
application.make_installed!
@@ -56,6 +64,14 @@ describe Clusters::Applications::Ingress do
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
end
end
+
+ context 'when there is already an external_hostname' do
+ let(:application) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
+
+ it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
+ expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
+ end
+ end
end
describe '#install_command' do
@@ -63,7 +79,7 @@ describe Clusters::Applications::Ingress do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with ingress arguments' do
+ it 'is initialized with ingress arguments' do
expect(subject.name).to eq('ingress')
expect(subject.chart).to eq('stable/nginx-ingress')
expect(subject.version).to eq('1.1.2')
@@ -82,7 +98,7 @@ describe Clusters::Applications::Ingress do
context 'application failed to install previously' do
let(:ingress) { create(:clusters_applications_ingress, :errored, version: 'nginx') }
- it 'should be initialized with the locked version' do
+ it 'is initialized with the locked version' do
expect(subject.version).to eq('1.1.2')
end
end
@@ -94,7 +110,7 @@ describe Clusters::Applications::Ingress do
subject { application.files }
- it 'should include ingress valid keys in values' do
+ it 'includes ingress valid keys in values' do
expect(values).to include('image')
expect(values).to include('repository')
expect(values).to include('stats')
diff --git a/spec/models/clusters/applications/jupyter_spec.rb b/spec/models/clusters/applications/jupyter_spec.rb
index 2c22c24c498..43fa1010b2b 100644
--- a/spec/models/clusters/applications/jupyter_spec.rb
+++ b/spec/models/clusters/applications/jupyter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Clusters::Applications::Jupyter do
@@ -8,6 +10,15 @@ describe Clusters::Applications::Jupyter do
it { is_expected.to belong_to(:oauth_application) }
+ describe '#can_uninstall?' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ subject { jupyter.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#set_initial_status' do
before do
jupyter.set_initial_status
@@ -26,6 +37,13 @@ describe Clusters::Applications::Jupyter do
it { expect(jupyter).to be_installable }
end
+
+ context 'when ingress is installed and external_hostname is assigned' do
+ let(:ingress) { create(:clusters_applications_ingress, :installed, external_hostname: 'localhost.localdomain') }
+ let(:jupyter) { create(:clusters_applications_jupyter, cluster: ingress.cluster) }
+
+ it { expect(jupyter).to be_installable }
+ end
end
describe '#install_command' do
@@ -36,10 +54,10 @@ describe Clusters::Applications::Jupyter do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with 4 arguments' do
+ it 'is initialized with 4 arguments' do
expect(subject.name).to eq('jupyter')
expect(subject.chart).to eq('jupyter/jupyterhub')
- expect(subject.version).to eq('v0.6')
+ expect(subject.version).to eq('0.9-174bbd5')
expect(subject).to be_rbac
expect(subject.repository).to eq('https://jupyterhub.github.io/helm-chart/')
expect(subject.files).to eq(jupyter.files)
@@ -56,8 +74,8 @@ describe Clusters::Applications::Jupyter do
context 'application failed to install previously' do
let(:jupyter) { create(:clusters_applications_jupyter, :errored, version: '0.0.1') }
- it 'should be initialized with the locked version' do
- expect(subject.version).to eq('v0.6')
+ it 'is initialized with the locked version' do
+ expect(subject.version).to eq('0.9-174bbd5')
end
end
end
@@ -68,7 +86,7 @@ describe Clusters::Applications::Jupyter do
subject { application.files }
- it 'should include valid values' do
+ it 'includes valid values' do
expect(values).to include('ingress')
expect(values).to include('hub')
expect(values).to include('rbac')
@@ -77,6 +95,9 @@ describe Clusters::Applications::Jupyter do
expect(values).to include('singleuser')
expect(values).to match(/clientId: '?#{application.oauth_application.uid}/)
expect(values).to match(/callbackUrl: '?#{application.callback_url}/)
+ expect(values).to include("gitlabProjectIdWhitelist:\n - #{application.cluster.project.id}")
+ expect(values).to include("c.GitLabOAuthenticator.scope = ['api read_repository write_repository']")
+ expect(values).to match(/GITLAB_HOST: '?#{Gitlab.config.gitlab.host}/)
end
context 'when cluster belongs to a project' do
diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb
index 006b922ab27..b38cf96de7e 100644
--- a/spec/models/clusters/applications/knative_spec.rb
+++ b/spec/models/clusters/applications/knative_spec.rb
@@ -1,9 +1,8 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Clusters::Applications::Knative do
- include KubernetesHelpers
- include ReactiveCachingHelpers
-
let(:knative) { create(:clusters_applications_knative) }
include_examples 'cluster application core specs', :clusters_applications_knative
@@ -37,6 +36,12 @@ describe Clusters::Applications::Knative do
end
end
+ describe '#can_uninstall?' do
+ subject { knative.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#schedule_status_update with external_ip' do
let(:application) { create(:clusters_applications_knative, :installed) }
@@ -64,23 +69,28 @@ describe Clusters::Applications::Knative do
expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
end
end
- end
- describe '#install_command' do
- subject { knative.install_command }
+ context 'when there is already an external_hostname' do
+ let(:application) { create(:clusters_applications_knative, :installed, external_hostname: 'localhost.localdomain') }
+
+ it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
+ expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
+ end
+ end
+ end
- it 'should be an instance of Helm::InstallCommand' do
+ shared_examples 'a command' do
+ it 'is an instance of Helm::InstallCommand' do
expect(subject).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand)
end
- it 'should be initialized with knative arguments' do
+ it 'is initialized with knative arguments' do
expect(subject.name).to eq('knative')
expect(subject.chart).to eq('knative/knative')
- expect(subject.version).to eq('0.2.2')
expect(subject.files).to eq(knative.files)
end
- it 'should not install metrics for prometheus' do
+ it 'does not install metrics for prometheus' do
expect(subject.postinstall).to be_nil
end
@@ -90,7 +100,7 @@ describe Clusters::Applications::Knative do
subject { knative.install_command }
- it 'should install metrics' do
+ it 'installs metrics' do
expect(subject.postinstall).not_to be_nil
expect(subject.postinstall.length).to be(1)
expect(subject.postinstall[0]).to eql("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}")
@@ -98,91 +108,39 @@ describe Clusters::Applications::Knative do
end
end
- describe '#files' do
- let(:application) { knative }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
+ describe '#install_command' do
+ subject { knative.install_command }
- it 'should include knative specific keys in the values.yaml file' do
- expect(values).to include('domain')
+ it 'is initialized with latest version' do
+ expect(subject.version).to eq('0.5.0')
end
- end
- describe 'validations' do
- it { is_expected.to validate_presence_of(:hostname) }
+ it_behaves_like 'a command'
end
- describe '#service_pod_details' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
- let(:namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- cluster_project: cluster.cluster_project,
- project: cluster.cluster_project.project)
- end
+ describe '#update_command' do
+ let!(:current_installed_version) { knative.version = '0.1.0' }
+ subject { knative.update_command }
- before do
- stub_kubeclient_discover(service.api_url)
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
- stub_reactive_cache(knative,
- {
- services: kube_response(kube_knative_services_body),
- pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
- })
- synchronous_reactive_cache(knative)
+ it 'is initialized with current version' do
+ expect(subject.version).to eq(current_installed_version)
end
- it 'should be able k8s core for pod details' do
- expect(knative.service_pod_details(namespace.namespace, cluster.cluster_project.project.name)).not_to be_nil
- end
+ it_behaves_like 'a command'
end
- describe '#services' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:service) { cluster.platform_kubernetes }
- let(:knative) { create(:clusters_applications_knative, cluster: cluster) }
-
- let(:namespace) do
- create(:cluster_kubernetes_namespace,
- cluster: cluster,
- cluster_project: cluster.cluster_project,
- project: cluster.cluster_project.project)
- end
-
- subject { knative.services }
+ describe '#files' do
+ let(:application) { knative }
+ let(:values) { subject[:'values.yaml'] }
- before do
- stub_kubeclient_discover(service.api_url)
- stub_kubeclient_knative_services
- stub_kubeclient_service_pods
- end
+ subject { application.files }
- it 'should have an unintialized cache' do
- is_expected.to be_nil
+ it 'includes knative specific keys in the values.yaml file' do
+ expect(values).to include('domain')
end
+ end
- context 'when using synchronous reactive cache' do
- before do
- stub_reactive_cache(knative,
- {
- services: kube_response(kube_knative_services_body),
- pods: kube_response(kube_knative_pods_body(cluster.cluster_project.project.name, namespace.namespace))
- })
- synchronous_reactive_cache(knative)
- end
-
- it 'should have cached services' do
- is_expected.not_to be_nil
- end
-
- it 'should match our namespace' do
- expect(knative.services_for(ns: namespace)).not_to be_nil
- end
- end
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:hostname) }
end
end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 81708b0c2ed..26267c64112 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Clusters::Applications::Prometheus do
@@ -9,6 +11,21 @@ describe Clusters::Applications::Prometheus do
include_examples 'cluster application helm specs', :clusters_applications_prometheus
include_examples 'cluster application initial status specs'
+ describe 'after_destroy' do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
+ let!(:application) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let!(:prometheus_service) { project.create_prometheus_service(active: true) }
+
+ it 'deactivates prometheus_service after destroy' do
+ expect do
+ application.destroy!
+
+ prometheus_service.reload
+ end.to change(prometheus_service, :active).from(true).to(false)
+ end
+ end
+
describe 'transition to installed' do
let(:project) { create(:project) }
let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
@@ -21,12 +38,20 @@ describe Clusters::Applications::Prometheus do
end
it 'ensures Prometheus service is activated' do
- expect(prometheus_service).to receive(:update).with(active: true)
+ expect(prometheus_service).to receive(:update!).with(active: true)
subject.make_installed
end
end
+ describe '#can_uninstall?' do
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ subject { prometheus.can_uninstall? }
+
+ it { is_expected.to be_truthy }
+ end
+
describe '#prometheus_client' do
context 'cluster is nil' do
it 'returns nil' do
@@ -92,7 +117,7 @@ describe Clusters::Applications::Prometheus do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with 3 arguments' do
+ it 'is initialized with 3 arguments' do
expect(subject.name).to eq('prometheus')
expect(subject.chart).to eq('stable/prometheus')
expect(subject.version).to eq('6.7.3')
@@ -111,12 +136,12 @@ describe Clusters::Applications::Prometheus do
context 'application failed to install previously' do
let(:prometheus) { create(:clusters_applications_prometheus, :errored, version: '2.0.0') }
- it 'should be initialized with the locked version' do
+ it 'is initialized with the locked version' do
expect(subject.version).to eq('6.7.3')
end
end
- it 'should not install knative metrics' do
+ it 'does not install knative metrics' do
expect(subject.postinstall).to be_nil
end
@@ -126,12 +151,40 @@ describe Clusters::Applications::Prometheus do
subject { prometheus.install_command }
- it 'should install knative metrics' do
+ it 'installs knative metrics' do
expect(subject.postinstall).to include("kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}")
end
end
end
+ describe '#uninstall_command' do
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ subject { prometheus.uninstall_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) }
+
+ it 'has the application name' do
+ expect(subject.name).to eq('prometheus')
+ end
+
+ it 'has files' do
+ expect(subject.files).to eq(prometheus.files)
+ end
+
+ it 'is rbac' do
+ expect(subject).to be_rbac
+ end
+
+ context 'on a non rbac enabled cluster' do
+ before do
+ prometheus.cluster.platform_kubernetes.abac!
+ end
+
+ it { is_expected.not_to be_rbac }
+ end
+ end
+
describe '#upgrade_command' do
let(:prometheus) { build(:clusters_applications_prometheus) }
let(:values) { prometheus.values }
@@ -140,7 +193,7 @@ describe Clusters::Applications::Prometheus do
expect(prometheus.upgrade_command(values)).to be_an_instance_of(::Gitlab::Kubernetes::Helm::InstallCommand)
end
- it 'should be initialized with 3 arguments' do
+ it 'is initialized with 3 arguments' do
command = prometheus.upgrade_command(values)
expect(command.name).to eq('prometheus')
@@ -178,7 +231,7 @@ describe Clusters::Applications::Prometheus do
subject { application.files }
- it 'should include prometheus valid values' do
+ it 'includes prometheus valid values' do
expect(values).to include('alertmanager')
expect(values).to include('kubeStateMetrics')
expect(values).to include('nodeExporter')
@@ -202,7 +255,7 @@ describe Clusters::Applications::Prometheus do
expect(subject[:'values.yaml']).to eq({ hello: :world })
end
- it 'should include cert files' do
+ it 'includes cert files' do
expect(subject[:'ca.pem']).to be_present
expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
@@ -218,7 +271,7 @@ describe Clusters::Applications::Prometheus do
application.cluster.application_helm.ca_cert = nil
end
- it 'should not include cert files' do
+ it 'does not include cert files' do
expect(subject[:'ca.pem']).not_to be_present
expect(subject[:'cert.pem']).not_to be_present
expect(subject[:'key.pem']).not_to be_present
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index 6972fc03415..4f0cd0efe9c 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Clusters::Applications::Runner do
@@ -11,6 +13,14 @@ describe Clusters::Applications::Runner do
it { is_expected.to belong_to(:runner) }
+ describe '#can_uninstall?' do
+ let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
+
+ subject { gitlab_runner.can_uninstall? }
+
+ it { is_expected.to be_falsey }
+ end
+
describe '#install_command' do
let(:kubeclient) { double('kubernetes client') }
let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
@@ -19,10 +29,10 @@ describe Clusters::Applications::Runner do
it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- it 'should be initialized with 4 arguments' do
+ it 'is initialized with 4 arguments' do
expect(subject.name).to eq('runner')
expect(subject.chart).to eq('runner/gitlab-runner')
- expect(subject.version).to eq('0.2.0')
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
expect(subject).to be_rbac
expect(subject.repository).to eq('https://charts.gitlab.io')
expect(subject.files).to eq(gitlab_runner.files)
@@ -39,8 +49,8 @@ describe Clusters::Applications::Runner do
context 'application failed to install previously' do
let(:gitlab_runner) { create(:clusters_applications_runner, :errored, runner: ci_runner, version: '0.1.13') }
- it 'should be initialized with the locked version' do
- expect(subject.version).to eq('0.2.0')
+ it 'is initialized with the locked version' do
+ expect(subject.version).to eq(Clusters::Applications::Runner::VERSION)
end
end
end
@@ -51,7 +61,7 @@ describe Clusters::Applications::Runner do
subject { application.files }
- it 'should include runner valid values' do
+ it 'includes runner valid values' do
expect(values).to include('concurrent')
expect(values).to include('checkInterval')
expect(values).to include('rbac')
@@ -59,29 +69,62 @@ describe Clusters::Applications::Runner do
expect(values).to include('privileged: true')
expect(values).to include('image: ubuntu:16.04')
expect(values).to include('resources')
- expect(values).to match(/runnerToken: '?#{ci_runner.token}/)
- expect(values).to match(/gitlabUrl: '?#{Gitlab::Routing.url_helpers.root_url}/)
+ expect(values).to match(/runnerToken: '?#{Regexp.escape(ci_runner.token)}/)
+ expect(values).to match(/gitlabUrl: '?#{Regexp.escape(Gitlab::Routing.url_helpers.root_url)}/)
end
context 'without a runner' do
- let(:project) { create(:project) }
- let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
let(:application) { create(:clusters_applications_runner, runner: nil, cluster: cluster) }
+ let(:runner) { application.runner }
+
+ shared_examples 'runner creation' do
+ it 'creates a runner' do
+ expect { subject }.to change { Ci::Runner.count }.by(1)
+ end
+
+ it 'uses the new runner token' do
+ expect(values).to match(/runnerToken: '?#{Regexp.escape(runner.token)}/)
+ end
+ end
+
+ context 'project cluster' do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster, :with_installed_helm, projects: [project]) }
- it 'creates a runner' do
- expect do
+ include_examples 'runner creation'
+
+ it 'creates a project runner' do
subject
- end.to change { Ci::Runner.count }.by(1)
+
+ expect(runner).to be_project_type
+ expect(runner.projects).to eq [project]
+ end
end
- it 'uses the new runner token' do
- expect(values).to match(/runnerToken: '?#{application.reload.runner.token}/)
+ context 'group cluster' do
+ let(:group) { create(:group) }
+ let(:cluster) { create(:cluster, :with_installed_helm, cluster_type: :group_type, groups: [group]) }
+
+ include_examples 'runner creation'
+
+ it 'creates a group runner' do
+ subject
+
+ expect(runner).to be_group_type
+ expect(runner.groups).to eq [group]
+ end
end
- it 'assigns the new runner to runner' do
- subject
+ context 'instance cluster' do
+ let(:cluster) { create(:cluster, :with_installed_helm, :instance) }
+
+ include_examples 'runner creation'
+
+ it 'creates an instance runner' do
+ subject
- expect(application.reload.runner).to be_project_type
+ expect(runner).to be_instance_type
+ end
end
end
@@ -108,7 +151,7 @@ describe Clusters::Applications::Runner do
allow(application).to receive(:chart_values).and_return(stub_values)
end
- it 'should overwrite values.yaml' do
+ it 'overwrites values.yaml' do
expect(values).to match(/privileged: '?#{application.privileged}/)
end
end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 3feed4e9718..f206bb41f45 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -2,9 +2,14 @@
require 'spec_helper'
-describe Clusters::Cluster do
+describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
+ include ReactiveCachingHelpers
+ include KubernetesHelpers
+
it_behaves_like 'having unique enum values'
+ subject { build(:cluster) }
+
it { is_expected.to belong_to(:user) }
it { is_expected.to have_many(:cluster_projects) }
it { is_expected.to have_many(:projects) }
@@ -17,12 +22,10 @@ describe Clusters::Cluster do
it { is_expected.to have_one(:application_prometheus) }
it { is_expected.to have_one(:application_runner) }
it { is_expected.to have_many(:kubernetes_namespaces) }
- it { is_expected.to have_one(:kubernetes_namespace) }
it { is_expected.to have_one(:cluster_project) }
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
- it { is_expected.to delegate_method(:status_name).to(:provider) }
it { is_expected.to delegate_method(:on_creation?).to(:provider) }
it { is_expected.to delegate_method(:active?).to(:platform_kubernetes).with_prefix }
it { is_expected.to delegate_method(:rbac?).to(:platform_kubernetes).with_prefix }
@@ -31,9 +34,15 @@ describe Clusters::Cluster do
it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix }
it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix }
+ it { is_expected.to delegate_method(:external_hostname).to(:application_ingress).with_prefix }
it { is_expected.to respond_to :project }
+ it do
+ expect(subject.knative_services_finder(subject.project))
+ .to be_instance_of(Clusters::KnativeServicesFinder)
+ end
+
describe '.enabled' do
subject { described_class.enabled }
@@ -94,6 +103,24 @@ describe Clusters::Cluster do
it { is_expected.to contain_exactly(cluster) }
end
+ describe '.managed' do
+ subject do
+ described_class.managed
+ end
+
+ context 'cluster is not managed' do
+ let!(:cluster) { create(:cluster, :not_managed) }
+
+ it { is_expected.not_to include(cluster) }
+ end
+
+ context 'cluster is managed' do
+ let!(:cluster) { create(:cluster) }
+
+ it { is_expected.to include(cluster) }
+ end
+ end
+
describe '.missing_kubernetes_namespace' do
let!(:cluster) { create(:cluster, :provided_by_gcp, :project) }
let(:project) { cluster.project }
@@ -268,7 +295,7 @@ describe Clusters::Cluster do
context 'when cluster is not a valid hostname' do
let(:cluster) { build(:cluster, domain: 'http://not.a.valid.hostname') }
- it 'should add an error on domain' do
+ it 'adds an error on domain' do
expect(subject).not_to be_valid
expect(subject.errors[:domain].first).to eq('contains invalid characters (valid characters: [a-z0-9\\-])')
end
@@ -306,6 +333,15 @@ describe Clusters::Cluster do
end
end
+ context 'when group and instance have configured kubernetes clusters' do
+ let(:project) { create(:project, group: group) }
+ let!(:instance_cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it 'returns clusters in order, descending the hierachy' do
+ is_expected.to eq([group_cluster, instance_cluster])
+ end
+ end
+
context 'when sub-group has configured kubernetes cluster', :nested_groups do
let(:sub_group_cluster) { create(:cluster, :provided_by_gcp, :group) }
let(:sub_group) { sub_group_cluster.group }
@@ -472,28 +508,6 @@ describe Clusters::Cluster do
end
end
- describe '#created?' do
- let(:cluster) { create(:cluster, :provided_by_gcp) }
-
- subject { cluster.created? }
-
- context 'when status_name is :created' do
- before do
- allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:created)
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'when status_name is not :created' do
- before do
- allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:creating)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
describe '#allow_user_defined_namespace?' do
let(:cluster) { create(:cluster, :provided_by_gcp) }
@@ -528,62 +542,15 @@ describe Clusters::Cluster do
end
context 'with no domain on cluster' do
- context 'with a project cluster' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- let(:project) { cluster.project }
-
- context 'with domain set at instance level' do
- before do
- stub_application_setting(auto_devops_domain: 'global_domain.com')
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
- it { is_expected.to eq('global_domain.com') }
- end
- end
-
- context 'with domain set on ProjectAutoDevops' do
- before do
- auto_devops = project.build_auto_devops(domain: 'legacy-ado-domain.com')
- auto_devops.save
- end
-
- it { is_expected.to eq('legacy-ado-domain.com') }
- end
-
- context 'with domain set as environment variable on project' do
- before do
- variable = project.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'project-ado-domain.com')
- variable.save
- end
-
- it { is_expected.to eq('project-ado-domain.com') }
+ context 'with domain set at instance level' do
+ before do
+ stub_application_setting(auto_devops_domain: 'global_domain.com')
end
- context 'with domain set as environment variable on the group project' do
- let(:group) { create(:group) }
-
- before do
- project.update(parent_id: group.id)
- variable = group.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'group-ado-domain.com')
- variable.save
- end
-
- it { is_expected.to eq('group-ado-domain.com') }
- end
- end
-
- context 'with a group cluster' do
- let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
-
- context 'with domain set as environment variable for the group' do
- let(:group) { cluster.group }
-
- before do
- variable = group.variables.build(key: 'AUTO_DEVOPS_DOMAIN', value: 'group-ado-domain.com')
- variable.save
- end
-
- it { is_expected.to eq('group-ado-domain.com') }
- end
+ it { is_expected.to eq('global_domain.com') }
end
end
end
@@ -598,7 +565,7 @@ describe Clusters::Cluster do
stub_application_setting(auto_devops_domain: 'global_domain.com')
end
- it 'should include KUBE_INGRESS_BASE_DOMAIN' do
+ it 'includes KUBE_INGRESS_BASE_DOMAIN' do
expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'global_domain.com')
end
end
@@ -606,7 +573,7 @@ describe Clusters::Cluster do
context 'with a cluster domain' do
let(:cluster) { create(:cluster, :provided_by_gcp, domain: 'example.com') }
- it 'should include KUBE_INGRESS_BASE_DOMAIN' do
+ it 'includes KUBE_INGRESS_BASE_DOMAIN' do
expect(subject.to_hash).to include(KUBE_INGRESS_BASE_DOMAIN: 'example.com')
end
end
@@ -614,9 +581,160 @@ describe Clusters::Cluster do
context 'with no domain' do
let(:cluster) { create(:cluster, :provided_by_gcp, :project) }
- it 'should return an empty array' do
+ it 'returns an empty array' do
expect(subject.to_hash).to be_empty
end
end
end
+
+ describe '#provided_by_user?' do
+ subject { cluster.provided_by_user? }
+
+ context 'with a GCP provider' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'with an user provider' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#status_name' do
+ subject { cluster.status_name }
+
+ context 'the cluster has a provider' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ before do
+ cluster.provider.make_errored!
+ end
+
+ it { is_expected.to eq :errored }
+ end
+
+ context 'there is a cached connection status' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ before do
+ allow(cluster).to receive(:connection_status).and_return(:connected)
+ end
+
+ it { is_expected.to eq :connected }
+ end
+
+ context 'there is no connection status in the cache' do
+ let(:cluster) { create(:cluster, :provided_by_user) }
+
+ before do
+ allow(cluster).to receive(:connection_status).and_return(nil)
+ end
+
+ it { is_expected.to eq :created }
+ end
+ end
+
+ describe '#connection_status' do
+ let(:cluster) { create(:cluster) }
+ let(:status) { :connected }
+
+ subject { cluster.connection_status }
+
+ it { is_expected.to be_nil }
+
+ context 'with a cached status' do
+ before do
+ stub_reactive_cache(cluster, connection_status: status)
+ end
+
+ it { is_expected.to eq(status) }
+ end
+ end
+
+ describe '#calculate_reactive_cache' do
+ subject { cluster.calculate_reactive_cache }
+
+ context 'cluster is disabled' do
+ let(:cluster) { create(:cluster, :disabled) }
+
+ it 'does not populate the cache' do
+ expect(cluster).not_to receive(:retrieve_connection_status)
+
+ is_expected.to be_nil
+ end
+ end
+
+ context 'cluster is enabled' do
+ let(:cluster) { create(:cluster, :provided_by_user, :group) }
+
+ context 'connection to the cluster is successful' do
+ before do
+ stub_kubeclient_discover(cluster.platform.api_url)
+ end
+
+ it { is_expected.to eq(connection_status: :connected) }
+ end
+
+ context 'cluster cannot be reached' do
+ before do
+ allow(cluster.kubeclient.core_client).to receive(:discover)
+ .and_raise(SocketError)
+ end
+
+ it { is_expected.to eq(connection_status: :unreachable) }
+ end
+
+ context 'cluster cannot be authenticated to' do
+ before do
+ allow(cluster.kubeclient.core_client).to receive(:discover)
+ .and_raise(OpenSSL::X509::CertificateError.new("Certificate error"))
+ end
+
+ it { is_expected.to eq(connection_status: :authentication_failure) }
+ end
+
+ describe 'Kubeclient::HttpError' do
+ let(:error_code) { 403 }
+ let(:error_message) { "Forbidden" }
+
+ before do
+ allow(cluster.kubeclient.core_client).to receive(:discover)
+ .and_raise(Kubeclient::HttpError.new(error_code, error_message, nil))
+ end
+
+ it { is_expected.to eq(connection_status: :authentication_failure) }
+
+ context 'generic timeout' do
+ let(:error_message) { 'Timed out connecting to server'}
+
+ it { is_expected.to eq(connection_status: :unreachable) }
+ end
+
+ context 'gateway timeout' do
+ let(:error_message) { '504 Gateway Timeout for GET https://kubernetes.example.com/api/v1'}
+
+ it { is_expected.to eq(connection_status: :unreachable) }
+ end
+ end
+
+ context 'an uncategorised error is raised' do
+ before do
+ allow(cluster.kubeclient.core_client).to receive(:discover)
+ .and_raise(StandardError)
+ end
+
+ it { is_expected.to eq(connection_status: :unknown_failure) }
+
+ it 'notifies Sentry' do
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception)
+ .with(instance_of(StandardError), hash_including(extra: { cluster_id: cluster.id }))
+
+ subject
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/clusters/kubernetes_namespace_spec.rb b/spec/models/clusters/kubernetes_namespace_spec.rb
index b865909c7fd..b5cba80b806 100644
--- a/spec/models/clusters/kubernetes_namespace_spec.rb
+++ b/spec/models/clusters/kubernetes_namespace_spec.rb
@@ -60,7 +60,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
context 'when platform has a namespace assigned' do
let(:namespace) { 'platform-namespace' }
- it 'should copy the namespace' do
+ it 'copies the namespace' do
subject
expect(kubernetes_namespace.namespace).to eq('platform-namespace')
@@ -72,7 +72,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
let(:namespace) { nil }
let(:project_slug) { "#{project.path}-#{project.id}" }
- it 'should fallback to project namespace' do
+ it 'fallbacks to project namespace' do
subject
expect(kubernetes_namespace.namespace).to eq(project_slug)
@@ -83,7 +83,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
describe '#service_account_name' do
let(:service_account_name) { "#{kubernetes_namespace.namespace}-service-account" }
- it 'should set a service account name based on namespace' do
+ it 'sets a service account name based on namespace' do
subject
expect(kubernetes_namespace.service_account_name).to eq(service_account_name)
@@ -115,7 +115,7 @@ RSpec.describe Clusters::KubernetesNamespace, type: :model do
expect(kubernetes_namespace.predefined_variables).to include(
{ key: 'KUBE_SERVICE_ACCOUNT', value: kubernetes_namespace.service_account_name, public: true },
{ key: 'KUBE_NAMESPACE', value: kubernetes_namespace.namespace, public: true },
- { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false },
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true },
{ key: 'KUBECONFIG', value: kubeconfig, public: false, file: true }
)
end
diff --git a/spec/models/clusters/platforms/kubernetes_spec.rb b/spec/models/clusters/platforms/kubernetes_spec.rb
index 4068d98d8f7..c485850c16e 100644
--- a/spec/models/clusters/platforms/kubernetes_spec.rb
+++ b/spec/models/clusters/platforms/kubernetes_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching do
@@ -13,10 +15,8 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to validate_presence_of(:api_url) }
it { is_expected.to validate_presence_of(:token) }
- it { is_expected.to delegate_method(:project).to(:cluster) }
it { is_expected.to delegate_method(:enabled?).to(:cluster) }
- it { is_expected.to delegate_method(:managed?).to(:cluster) }
- it { is_expected.to delegate_method(:kubernetes_namespace).to(:cluster) }
+ it { is_expected.to delegate_method(:provided_by_user?).to(:cluster) }
it_behaves_like 'having unique enum values'
@@ -98,6 +98,22 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { expect(kubernetes.save).to be_truthy }
end
+
+ context 'when api_url is localhost' do
+ let(:api_url) { 'http://localhost:22' }
+
+ it { expect(kubernetes.save).to be_falsey }
+
+ context 'Application settings allows local requests' do
+ before do
+ allow(ApplicationSetting)
+ .to receive(:current)
+ .and_return(ApplicationSetting.build_from_defaults(allow_local_requests_from_hooks_and_services: true))
+ end
+
+ it { expect(kubernetes.save).to be_truthy }
+ end
+ end
end
context 'when validates token' do
@@ -191,7 +207,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it { is_expected.to be_truthy }
end
- describe '#actual_namespace' do
+ describe '#kubernetes_namespace_for' do
let(:cluster) { create(:cluster, :project) }
let(:project) { cluster.project }
@@ -201,7 +217,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
namespace: namespace)
end
- subject { platform.actual_namespace }
+ subject { platform.kubernetes_namespace_for(project) }
context 'with a namespace assigned' do
let(:namespace) { 'namespace-123' }
@@ -253,7 +269,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it 'sets KUBE_TOKEN' do
expect(subject).to include(
- { key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
)
end
end
@@ -265,7 +281,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it 'sets KUBE_TOKEN' do
expect(subject).to include(
- { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false }
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true }
)
end
end
@@ -281,19 +297,17 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it 'sets KUBE_TOKEN' do
expect(subject).to include(
- { key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
)
end
end
context 'no namespace provided' do
- let(:namespace) { kubernetes.actual_namespace }
-
it_behaves_like 'setting variables'
it 'sets KUBE_TOKEN' do
expect(subject).to include(
- { key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
)
end
end
@@ -313,6 +327,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
{ key: 'KUBE_TOKEN', value: kubernetes.token, public: false }
)
end
+
+ context 'the cluster is not managed' do
+ let!(:cluster) { create(:cluster, :group, :not_managed, platform_kubernetes: kubernetes) }
+
+ it_behaves_like 'setting variables'
+
+ it 'sets KUBE_TOKEN' do
+ expect(subject).to include(
+ { key: 'KUBE_TOKEN', value: kubernetes.token, public: false, masked: true }
+ )
+ end
+ end
end
context 'kubernetes namespace exists for the project' do
@@ -322,7 +348,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
it 'sets KUBE_TOKEN' do
expect(subject).to include(
- { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false }
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true }
)
end
end
@@ -359,14 +385,14 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
context 'with valid pods' do
- let(:pod) { kube_pod(app: environment.slug) }
- let(:pod_with_no_terminal) { kube_pod(app: environment.slug, status: "Pending") }
+ let(:pod) { kube_pod(environment_slug: environment.slug, namespace: cluster.kubernetes_namespace_for(project), project_slug: project.full_path_slug) }
+ let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") }
let(:terminals) { kube_terminals(service, pod) }
before do
stub_reactive_cache(
service,
- pods: [pod, pod, pod_with_no_terminal, kube_pod(app: "should-be-filtered-out")]
+ pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")]
)
end
@@ -389,6 +415,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
let!(:cluster) { create(:cluster, :project, enabled: enabled, platform_kubernetes: service) }
let(:service) { create(:cluster_platform_kubernetes, :configured) }
let(:enabled) { true }
+ let(:namespace) { cluster.kubernetes_namespace_for(cluster.project) }
context 'when cluster is disabled' do
let(:enabled) { false }
@@ -398,8 +425,8 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with valid pods and deployments' do
before do
- stub_kubeclient_pods
- stub_kubeclient_deployments
+ stub_kubeclient_pods(namespace)
+ stub_kubeclient_deployments(namespace)
end
it { is_expected.to include(pods: [kube_pod]) }
@@ -407,8 +434,8 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 500s' do
before do
- stub_kubeclient_pods(status: 500)
- stub_kubeclient_deployments(status: 500)
+ stub_kubeclient_pods(namespace, status: 500)
+ stub_kubeclient_deployments(namespace, status: 500)
end
it { expect { subject }.to raise_error(Kubeclient::HttpError) }
@@ -416,12 +443,18 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
context 'when kubernetes responds with 404s' do
before do
- stub_kubeclient_pods(status: 404)
- stub_kubeclient_deployments(status: 404)
+ stub_kubeclient_pods(namespace, status: 404)
+ stub_kubeclient_deployments(namespace, status: 404)
end
it { is_expected.to include(pods: []) }
end
+
+ context 'when the cluster is not project level' do
+ let(:cluster) { create(:cluster, :group, platform_kubernetes: service) }
+
+ it { is_expected.to include(pods: []) }
+ end
end
describe '#update_kubernetes_namespace' do
@@ -429,7 +462,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
let(:platform) { cluster.platform }
context 'when namespace is updated' do
- it 'should call ConfigureWorker' do
+ it 'calls ConfigureWorker' do
expect(ClusterConfigureWorker).to receive(:perform_async).with(cluster.id).once
platform.namespace = 'new-namespace'
@@ -438,7 +471,7 @@ describe Clusters::Platforms::Kubernetes, :use_clean_rails_memory_store_caching
end
context 'when namespace is not updated' do
- it 'should not call ConfigureWorker' do
+ it 'does not call ConfigureWorker' do
expect(ClusterConfigureWorker).not_to receive(:perform_async)
platform.username = "new-username"
diff --git a/spec/models/clusters/project_spec.rb b/spec/models/clusters/project_spec.rb
index 82ef5a23c18..671af085d10 100644
--- a/spec/models/clusters/project_spec.rb
+++ b/spec/models/clusters/project_spec.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Project do
it { is_expected.to belong_to(:cluster) }
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:kubernetes_namespaces) }
- it { is_expected.to have_one(:kubernetes_namespace) }
end
diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb
index 5012e6f15c6..785db4febe0 100644
--- a/spec/models/clusters/providers/gcp_spec.rb
+++ b/spec/models/clusters/providers/gcp_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Providers::Gcp do
diff --git a/spec/models/commit_collection_spec.rb b/spec/models/commit_collection_spec.rb
index 0f5d03ff458..0bdf83fa90f 100644
--- a/spec/models/commit_collection_spec.rb
+++ b/spec/models/commit_collection_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CommitCollection do
@@ -12,37 +14,117 @@ describe CommitCollection do
end
end
- describe '.authors' do
+ describe '.committers' do
it 'returns a relation of users when users are found' do
- user = create(:user, email: commit.author_email.upcase)
+ user = create(:user, email: commit.committer_email.upcase)
collection = described_class.new(project, [commit])
- expect(collection.authors).to contain_exactly(user)
+ expect(collection.committers).to contain_exactly(user)
end
- it 'returns empty array when authors cannot be found' do
+ it 'returns empty array when committers cannot be found' do
collection = described_class.new(project, [commit])
- expect(collection.authors).to be_empty
+ expect(collection.committers).to be_empty
end
it 'excludes authors of merge commits' do
commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98")
- create(:user, email: commit.author_email.upcase)
+ create(:user, email: commit.committer_email.upcase)
collection = described_class.new(project, [commit])
- expect(collection.authors).to be_empty
+ expect(collection.committers).to be_empty
end
end
describe '#without_merge_commits' do
it 'returns all commits except merge commits' do
+ merge_commit = project.commit("60ecb67744cb56576c30214ff52294f8ce2def98")
+ expect(merge_commit).to receive(:merge_commit?).and_return(true)
+
collection = described_class.new(project, [
- build(:commit),
- build(:commit, :merge_commit)
+ commit,
+ merge_commit
])
- expect(collection.without_merge_commits.size).to eq(1)
+ expect(collection.without_merge_commits).to contain_exactly(commit)
+ end
+ end
+
+ describe 'enrichment methods' do
+ let(:gitaly_commit) { commit }
+ let(:hash_commit) { Commit.from_hash(gitaly_commit.to_hash, project) }
+
+ describe '#unenriched' do
+ it 'returns all commits that are not backed by gitaly data' do
+ collection = described_class.new(project, [gitaly_commit, hash_commit])
+
+ expect(collection.unenriched).to contain_exactly(hash_commit)
+ end
+ end
+
+ describe '#fully_enriched?' do
+ it 'returns true when all commits are backed by gitaly data' do
+ collection = described_class.new(project, [gitaly_commit, gitaly_commit])
+
+ expect(collection.fully_enriched?).to eq(true)
+ end
+
+ it 'returns false when any commits are not backed by gitaly data' do
+ collection = described_class.new(project, [gitaly_commit, hash_commit])
+
+ expect(collection.fully_enriched?).to eq(false)
+ end
+
+ it 'returns true when the collection is empty' do
+ collection = described_class.new(project, [])
+
+ expect(collection.fully_enriched?).to eq(true)
+ end
+ end
+
+ describe '#enrich!' do
+ it 'replaces commits in the collection with those backed by gitaly data' do
+ collection = described_class.new(project, [hash_commit])
+
+ collection.enrich!
+
+ new_commit = collection.commits.first
+ expect(new_commit.id).to eq(hash_commit.id)
+ expect(hash_commit.gitaly_commit?).to eq(false)
+ expect(new_commit.gitaly_commit?).to eq(true)
+ end
+
+ it 'maintains the original order of the commits' do
+ gitaly_commits = [gitaly_commit] * 3
+ hash_commits = [hash_commit] * 3
+ # Interleave the gitaly and hash commits together
+ original_commits = gitaly_commits.zip(hash_commits).flatten
+ collection = described_class.new(project, original_commits)
+
+ collection.enrich!
+
+ original_commits.each_with_index do |original_commit, i|
+ new_commit = collection.commits[i]
+ expect(original_commit.id).to eq(new_commit.id)
+ end
+ end
+
+ it 'fetches data if there are unenriched commits' do
+ collection = described_class.new(project, [hash_commit])
+
+ expect(Commit).to receive(:lazy).exactly(:once)
+
+ collection.enrich!
+ end
+
+ it 'does not fetch data if all commits are enriched' do
+ collection = described_class.new(project, [gitaly_commit])
+
+ expect(Commit).not_to receive(:lazy)
+
+ collection.enrich!
+ end
end
end
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index f2efcd9d0e9..b96ca89c893 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CommitRange do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index baad8352185..14f4b4d692f 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Commit do
@@ -542,7 +544,7 @@ eos
end
end
- describe '#uri_type' do
+ shared_examples '#uri_type' do
it 'returns the URI type at the given path' do
expect(commit.uri_type('files/html')).to be(:tree)
expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
@@ -561,6 +563,20 @@ eos
end
end
+ describe '#uri_type with Gitaly enabled' do
+ it_behaves_like "#uri_type"
+ end
+
+ describe '#uri_type with Rugged enabled', :enable_rugged do
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged::Tree).to receive(:path).with('files/html').and_call_original
+
+ commit.uri_type('files/html')
+ end
+
+ it_behaves_like '#uri_type'
+ end
+
describe '.from_hash' do
let(:new_commit) { described_class.from_hash(commit.to_hash, project) }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 8b7c88805c1..017cca0541e 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CommitStatus do
@@ -49,6 +51,16 @@ describe CommitStatus do
commit_status.success!
end
+
+ describe 'transitioning to running' do
+ let(:commit_status) { create(:commit_status, :pending, started_at: nil) }
+
+ it 'records the started at time' do
+ commit_status.run!
+
+ expect(commit_status.started_at).to be_present
+ end
+ end
end
describe '#started?' do
@@ -437,7 +449,7 @@ describe CommitStatus do
end
it "lock" do
- is_expected.to be true
+ is_expected.to be_truthy
end
it "raise exception when trying to update" do
@@ -451,7 +463,7 @@ describe CommitStatus do
end
it "do not lock" do
- is_expected.to be false
+ is_expected.to be_falsey
end
it "save correctly" do
@@ -479,6 +491,12 @@ describe CommitStatus do
it { is_expected.to be_script_failure }
end
+
+ context 'when failure_reason is unmet_prerequisites' do
+ let(:reason) { :unmet_prerequisites }
+
+ it { is_expected.to be_unmet_prerequisites }
+ end
end
describe 'ensure stage assignment' do
@@ -555,6 +573,7 @@ describe CommitStatus do
before do
allow(Time).to receive(:now).and_return(current_time)
+ expect(commit_status.any_unmet_prerequisites?).to eq false
end
shared_examples 'commit status enqueued' do
@@ -569,6 +588,12 @@ describe CommitStatus do
it_behaves_like 'commit status enqueued'
end
+ context 'when initial state is :preparing' do
+ let(:commit_status) { create(:commit_status, :preparing) }
+
+ it_behaves_like 'commit status enqueued'
+ end
+
context 'when initial state is :skipped' do
let(:commit_status) { create(:commit_status, :skipped) }
diff --git a/spec/models/compare_spec.rb b/spec/models/compare_spec.rb
index 0bc3ee014e6..43c3580bed2 100644
--- a/spec/models/compare_spec.rb
+++ b/spec/models/compare_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Compare do
diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb
index 04d6cfa2c02..de2bc3a387b 100644
--- a/spec/models/concerns/access_requestable_spec.rb
+++ b/spec/models/concerns/access_requestable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AccessRequestable do
diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb
index 1ea7f2b9985..c750be6b75c 100644
--- a/spec/models/concerns/avatarable_spec.rb
+++ b/spec/models/concerns/avatarable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Avatarable do
diff --git a/spec/models/concerns/awardable_spec.rb b/spec/models/concerns/awardable_spec.rb
index 5713106418d..9e7106281ee 100644
--- a/spec/models/concerns/awardable_spec.rb
+++ b/spec/models/concerns/awardable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Awardable do
diff --git a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
index e5392fe0462..1fe90d3cc9a 100644
--- a/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
+++ b/spec/models/concerns/batch_destroy_dependent_associations_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BatchDestroyDependentAssociations do
diff --git a/spec/models/concerns/blocks_json_serialization_spec.rb b/spec/models/concerns/blocks_json_serialization_spec.rb
index 5906b588d0e..e8657c45a93 100644
--- a/spec/models/concerns/blocks_json_serialization_spec.rb
+++ b/spec/models/concerns/blocks_json_serialization_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe BlocksJsonSerialization do
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 447279f19a8..0e5fb2b5153 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -1,376 +1,214 @@
-require 'spec_helper'
-
-describe CacheMarkdownField do
- # The minimum necessary ActiveModel to test this concern
- class ThingWithMarkdownFields
- include ActiveModel::Model
- include ActiveModel::Dirty
+# frozen_string_literal: true
- include ActiveModel::Serialization
-
- class_attribute :attribute_names
- self.attribute_names = []
+require 'spec_helper'
- def attributes
- attribute_names.each_with_object({}) do |name, hsh|
- hsh[name.to_s] = send(name)
- end
+describe CacheMarkdownField, :clean_gitlab_redis_cache do
+ let(:ar_class) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'issues'
+ include CacheMarkdownField
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
end
+ end
- extend ActiveModel::Callbacks
- define_model_callbacks :create, :update
-
- include CacheMarkdownField
- cache_markdown_field :foo
- cache_markdown_field :baz, pipeline: :single_line
+ let(:other_class) do
+ Class.new do
+ include CacheMarkdownField
- def self.add_attr(name)
- self.attribute_names += [name]
- define_attribute_methods(name)
- attr_reader(name)
- define_method("#{name}=") do |value|
- write_attribute(name, value)
+ def initialize(args = {})
+ @title, @description, @cached_markdown_version = args[:title], args[:description], args[:cached_markdown_version]
+ @title_html, @description_html = args[:title_html], args[:description_html]
+ @author, @project = args[:author], args[:project]
end
- end
- add_attr :cached_markdown_version
+ attr_accessor :title, :description, :cached_markdown_version
- [:foo, :foo_html, :bar, :baz, :baz_html].each do |name|
- add_attr(name)
- end
-
- def initialize(*)
- super
-
- # Pretend new is load
- clear_changes_information
- end
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
- def read_attribute(name)
- instance_variable_get("@#{name}")
- end
-
- def write_attribute(name, value)
- send("#{name}_will_change!") unless value == read_attribute(name)
- instance_variable_set("@#{name}", value)
- end
-
- def save
- run_callbacks :update do
- changes_applied
+ def cache_key
+ "cache-key"
end
end
-
- def has_attribute?(attr_name)
- attribute_names.include?(attr_name)
- end
- end
-
- def thing_subclass(new_attr)
- Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
end
let(:markdown) { '`Foo`' }
- let(:html) { '<p dir="auto"><code>Foo</code></p>' }
+ let(:html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Foo</code></p>' }
let(:updated_markdown) { '`Bar`' }
- let(:updated_html) { '<p dir="auto"><code>Bar</code></p>' }
-
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
- let(:cache_version) { CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 }
-
- before do
- stub_commonmark_sourcepos_disabled
- end
-
- describe '.attributes' do
- it 'excludes cache attributes' do
- expect(thing.attributes.keys.sort).to eq(%w[bar baz foo])
- end
- end
-
- context 'an unchanged markdown field' do
- before do
- thing.foo = thing.foo
- thing.save
- end
-
- it { expect(thing.foo).to eq(markdown) }
- it { expect(thing.foo_html).to eq(html) }
- it { expect(thing.foo_html_changed?).not_to be_truthy }
- it { expect(thing.cached_markdown_version).to eq(cache_version) }
- end
-
- context 'a changed markdown field' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
-
- before do
- thing.foo = updated_markdown
- thing.save
- end
+ let(:updated_html) { '<p data-sourcepos="1:1-1:5" dir="auto"><code>Bar</code></p>' }
- it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(cache_version) }
- end
+ let(:cache_version) { Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
- context 'when a markdown field is set repeatedly to an empty string' do
- it do
- expect(thing).to receive(:refresh_markdown_cache).once
- thing.foo = ''
- thing.save
- thing.foo = ''
- thing.save
- end
+ def thing_subclass(klass, extra_attribute)
+ Class.new(klass) { attr_accessor(extra_attribute) }
end
- context 'when a markdown field is set repeatedly to a string which renders as empty html' do
- it do
- expect(thing).to receive(:refresh_markdown_cache).once
- thing.foo = '[//]: # (This is also a comment.)'
- thing.save
- thing.foo = '[//]: # (This is also a comment.)'
- thing.save
- end
- end
+ shared_examples 'a class with cached markdown fields' do
+ describe '#cached_html_up_to_date?' do
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
- context 'when a markdown field and html field are both changed' do
- it do
- expect(thing).not_to receive(:refresh_markdown_cache)
- thing.foo = '_look over there!_'
- thing.foo_html = '<em>look over there!</em>'
- thing.save
- end
- end
-
- context 'a non-markdown field changed' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version - 1) }
-
- before do
- thing.bar = 'OK'
- thing.save
- end
-
- it { expect(thing.bar).to eq('OK') }
- it { expect(thing.foo).to eq(markdown) }
- it { expect(thing.foo_html).to eq(html) }
- it { expect(thing.cached_markdown_version).to eq(cache_version) }
- end
+ subject { thing.cached_html_up_to_date?(:title) }
- context 'version is out of date' do
- let(:thing) { ThingWithMarkdownFields.new(foo: updated_markdown, foo_html: html, cached_markdown_version: nil) }
+ it 'returns false when the version is absent' do
+ thing.cached_markdown_version = nil
- before do
- thing.save
- end
-
- it { expect(thing.foo_html).to eq(updated_html) }
- it { expect(thing.cached_markdown_version).to eq(cache_version) }
- end
-
- describe '#cached_html_up_to_date?' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
-
- subject { thing.cached_html_up_to_date?(:foo) }
-
- it 'returns false when the version is absent' do
- thing.cached_markdown_version = nil
-
- is_expected.to be_falsy
- end
-
- it 'returns false when the cached version is too old' do
- thing.cached_markdown_version = cache_version - 1
-
- is_expected.to be_falsy
- end
-
- it 'returns false when the cached version is in future' do
- thing.cached_markdown_version = cache_version + 1
-
- is_expected.to be_falsy
- end
-
- it 'returns false when the local version was bumped' do
- allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
- thing.cached_markdown_version = cache_version
-
- is_expected.to be_falsy
- end
-
- it 'returns true when the local version is default' do
- thing.cached_markdown_version = cache_version
-
- is_expected.to be_truthy
- end
+ is_expected.to be_falsy
+ end
- it 'returns true when the cached version is just right' do
- allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
- thing.cached_markdown_version = cache_version + 2
+ it 'returns false when the version is too early' do
+ thing.cached_markdown_version -= 1
- is_expected.to be_truthy
- end
+ is_expected.to be_falsy
+ end
- it 'returns false if markdown has been changed but html has not' do
- thing.foo = updated_html
+ it 'returns false when the version is too late' do
+ thing.cached_markdown_version += 1
- is_expected.to be_falsy
- end
+ is_expected.to be_falsy
+ end
- it 'returns true if markdown has not been changed but html has' do
- thing.foo_html = updated_html
+ it 'returns false when the local version was bumped' do
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
+ thing.cached_markdown_version = cache_version
- is_expected.to be_truthy
- end
+ is_expected.to be_falsy
+ end
- it 'returns true if markdown and html have both been changed' do
- thing.foo = updated_markdown
- thing.foo_html = updated_html
+ it 'returns true when the local version is default' do
+ thing.cached_markdown_version = cache_version
- is_expected.to be_truthy
- end
+ is_expected.to be_truthy
+ end
- it 'returns false if the markdown field is set but the html is not' do
- thing.foo_html = nil
+ it 'returns true when the cached version is just right' do
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:local_markdown_version).and_return(2)
+ thing.cached_markdown_version = cache_version + 2
- is_expected.to be_falsy
+ is_expected.to be_truthy
+ end
end
- end
- describe '#latest_cached_markdown_version' do
- subject { thing.latest_cached_markdown_version }
+ describe '#latest_cached_markdown_version' do
+ let(:thing) { klass.new }
+ subject { thing.latest_cached_markdown_version }
- it 'returns default version' do
- thing.cached_markdown_version = nil
- is_expected.to eq(cache_version)
- end
- end
-
- describe '#refresh_markdown_cache' do
- before do
- thing.foo = updated_markdown
+ it 'returns default version' do
+ thing.cached_markdown_version = nil
+ is_expected.to eq(cache_version)
+ end
end
- it 'fills all html fields' do
- thing.refresh_markdown_cache
+ describe '#refresh_markdown_cache' do
+ let(:thing) { klass.new(description: markdown, description_html: html, cached_markdown_version: cache_version) }
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.foo_html_changed?).to be_truthy
- expect(thing.baz_html_changed?).to be_truthy
- end
+ before do
+ thing.description = updated_markdown
+ end
- it 'does not save the result' do
- expect(thing).not_to receive(:update_columns)
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache
- thing.refresh_markdown_cache
- end
+ expect(thing.description_html).to eq(updated_html)
+ end
- it 'updates the markdown cache version' do
- thing.cached_markdown_version = nil
- thing.refresh_markdown_cache
+ it 'does not save the result' do
+ expect(thing).not_to receive(:save_markdown)
- expect(thing.cached_markdown_version).to eq(cache_version)
- end
- end
+ thing.refresh_markdown_cache
+ end
- describe '#refresh_markdown_cache!' do
- let(:thing) { ThingWithMarkdownFields.new(foo: markdown, foo_html: html, cached_markdown_version: cache_version) }
+ it 'updates the markdown cache version' do
+ thing.cached_markdown_version = nil
+ thing.refresh_markdown_cache
- before do
- thing.foo = updated_markdown
+ expect(thing.cached_markdown_version).to eq(cache_version)
+ end
end
- it 'fills all html fields' do
- thing.refresh_markdown_cache!
+ describe '#refresh_markdown_cache!' do
+ let(:thing) { klass.new(description: markdown, description_html: html, cached_markdown_version: cache_version) }
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.foo_html_changed?).to be_truthy
- expect(thing.baz_html_changed?).to be_truthy
- end
+ before do
+ thing.description = updated_markdown
+ end
- it 'skips saving if not persisted' do
- expect(thing).to receive(:persisted?).and_return(false)
- expect(thing).not_to receive(:update_columns)
+ it 'fills all html fields' do
+ thing.refresh_markdown_cache!
- thing.refresh_markdown_cache!
- end
+ expect(thing.description_html).to eq(updated_html)
+ end
- it 'saves the changes using #update_columns' do
- expect(thing).to receive(:persisted?).and_return(true)
- expect(thing).to receive(:update_columns)
- .with("foo_html" => updated_html, "baz_html" => "", "cached_markdown_version" => cache_version)
+ it 'saves the changes' do
+ expect(thing)
+ .to receive(:save_markdown)
+ .with("description_html" => updated_html, "title_html" => "", "cached_markdown_version" => cache_version)
- thing.refresh_markdown_cache!
+ thing.refresh_markdown_cache!
+ end
end
- end
- describe '#banzai_render_context' do
- subject(:context) { thing.banzai_render_context(:foo) }
+ describe '#banzai_render_context' do
+ let(:thing) { klass.new(title: markdown, title_html: html, cached_markdown_version: cache_version) }
+ subject(:context) { thing.banzai_render_context(:title) }
- it 'sets project to nil if the object lacks a project' do
- is_expected.to have_key(:project)
- expect(context[:project]).to be_nil
- end
+ it 'sets project to nil if the object lacks a project' do
+ is_expected.to have_key(:project)
+ expect(context[:project]).to be_nil
+ end
- it 'excludes author if the object lacks an author' do
- is_expected.not_to have_key(:author)
- end
+ it 'excludes author if the object lacks an author' do
+ is_expected.not_to have_key(:author)
+ end
- it 'raises if the context for an unrecognised field is requested' do
- expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
- end
+ it 'raises if the context for an unrecognised field is requested' do
+ expect { thing.banzai_render_context(:not_found) }.to raise_error(ArgumentError)
+ end
- it 'includes the pipeline' do
- baz = thing.banzai_render_context(:baz)
+ it 'includes the pipeline' do
+ title_context = thing.banzai_render_context(:title)
- expect(baz[:pipeline]).to eq(:single_line)
- end
+ expect(title_context[:pipeline]).to eq(:single_line)
+ end
- it 'returns copies of the context template' do
- template = thing.cached_markdown_fields[:baz]
- copy = thing.banzai_render_context(:baz)
+ it 'returns copies of the context template' do
+ template = thing.cached_markdown_fields[:description]
+ copy = thing.banzai_render_context(:description)
- expect(copy).not_to be(template)
- end
+ expect(copy).not_to be(template)
+ end
- context 'with a project' do
- let(:project) { create(:project, group: create(:group)) }
- let(:thing) { thing_subclass(:project).new(foo: markdown, foo_html: html, project: project) }
+ context 'with a project' do
+ let(:project) { build(:project, group: create(:group)) }
+ let(:thing) { thing_subclass(klass, :project).new(title: markdown, title_html: html, project: project) }
- it 'sets the project in the context' do
- is_expected.to have_key(:project)
- expect(context[:project]).to eq(project)
+ it 'sets the project in the context' do
+ is_expected.to have_key(:project)
+ expect(context[:project]).to eq(project)
+ end
end
- it 'invalidates the cache when project changes' do
- thing.project = :new_project
- allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
-
- thing.save
+ context 'with an author' do
+ let(:thing) { thing_subclass(klass, :author).new(title: markdown, title_html: html, author: :author_value) }
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(cache_version)
+ it 'sets the author in the context' do
+ is_expected.to have_key(:author)
+ expect(context[:author]).to eq(:author_value)
+ end
end
end
+ end
- context 'with an author' do
- let(:thing) { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author_value) }
-
- it 'sets the author in the context' do
- is_expected.to have_key(:author)
- expect(context[:author]).to eq(:author_value)
- end
+ context 'for Active record classes' do
+ let(:klass) { ar_class }
- it 'invalidates the cache when author changes' do
- thing.author = :new_author
- allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+ it_behaves_like 'a class with cached markdown fields'
+ end
- thing.save
+ context 'for other classes' do
+ let(:klass) { other_class }
- expect(thing.foo_html).to eq(updated_html)
- expect(thing.baz_html).to eq(updated_html)
- expect(thing.cached_markdown_version).to eq(cache_version)
- end
- end
+ it_behaves_like 'a class with cached markdown fields'
end
end
diff --git a/spec/models/concerns/cacheable_attributes_spec.rb b/spec/models/concerns/cacheable_attributes_spec.rb
index 43a544cfe26..394fac52aa7 100644
--- a/spec/models/concerns/cacheable_attributes_spec.rb
+++ b/spec/models/concerns/cacheable_attributes_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CacheableAttributes do
diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb
index 1bf6c9b3404..d6d41a25eac 100644
--- a/spec/models/concerns/case_sensitivity_spec.rb
+++ b/spec/models/concerns/case_sensitivity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CaseSensitivity do
diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb
index 51221e07ca3..e41d75568f7 100644
--- a/spec/models/concerns/chronic_duration_attribute_spec.rb
+++ b/spec/models/concerns/chronic_duration_attribute_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
shared_examples 'ChronicDurationAttribute reader' do
diff --git a/spec/models/concerns/deployable_spec.rb b/spec/models/concerns/deployable_spec.rb
index 6951be903fe..42bed9434f5 100644
--- a/spec/models/concerns/deployable_spec.rb
+++ b/spec/models/concerns/deployable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Deployable do
diff --git a/spec/models/concerns/deployment_platform_spec.rb b/spec/models/concerns/deployment_platform_spec.rb
index 19ab4382b53..0e34d8fccf3 100644
--- a/spec/models/concerns/deployment_platform_spec.rb
+++ b/spec/models/concerns/deployment_platform_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe DeploymentPlatform do
diff --git a/spec/models/concerns/deprecated_assignee_spec.rb b/spec/models/concerns/deprecated_assignee_spec.rb
new file mode 100644
index 00000000000..e394de0aa34
--- /dev/null
+++ b/spec/models/concerns/deprecated_assignee_spec.rb
@@ -0,0 +1,160 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeprecatedAssignee do
+ let(:user) { create(:user) }
+
+ describe '#assignee_id=' do
+ it 'creates the merge_request_assignees relation' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ merge_request.reload
+
+ expect(merge_request.merge_request_assignees.count).to eq(1)
+ end
+
+ it 'nullifies the assignee_id column' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ merge_request.reload
+
+ expect(merge_request.read_attribute(:assignee_id)).to be_nil
+ end
+
+ context 'when relation already exists' do
+ it 'overwrites existing assignees' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.merge_request_assignees.create!(user_id: user.id)
+ merge_request.merge_request_assignees.create!(user_id: other_user.id)
+
+ expect { merge_request.update!(assignee_id: other_user.id) }
+ .to change { merge_request.reload.merge_request_assignees.count }
+ .from(2).to(1)
+ end
+ end
+ end
+
+ describe '#assignee=' do
+ it 'creates the merge_request_assignees relation' do
+ merge_request = create(:merge_request, assignee: user)
+
+ merge_request.reload
+
+ expect(merge_request.merge_request_assignees.count).to eq(1)
+ end
+
+ it 'nullifies the assignee_id column' do
+ merge_request = create(:merge_request, assignee: user)
+
+ merge_request.reload
+
+ expect(merge_request.read_attribute(:assignee_id)).to be_nil
+ end
+
+ context 'when relation already exists' do
+ it 'overwrites existing assignees' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignee: nil)
+ merge_request.merge_request_assignees.create!(user_id: user.id)
+ merge_request.merge_request_assignees.create!(user_id: other_user.id)
+
+ expect { merge_request.update!(assignee: other_user) }
+ .to change { merge_request.reload.merge_request_assignees.count }
+ .from(2).to(1)
+ end
+ end
+ end
+
+ describe '#assignee_id' do
+ it 'returns the first assignee ID' do
+ other_user = create(:user)
+ merge_request = create(:merge_request, assignees: [user, other_user])
+
+ merge_request.reload
+
+ expect(merge_request.assignee_id).to eq(merge_request.assignee_ids.first)
+ end
+ end
+
+ describe '#assignees' do
+ context 'when assignee_id exists and there is no relation' do
+ it 'creates the relation' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignees }.to change { merge_request.merge_request_assignees.count }.from(0).to(1)
+ end
+
+ it 'nullifies the assignee_id' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignees }
+ .to change { merge_request.read_attribute(:assignee_id) }
+ .from(user.id).to(nil)
+ end
+ end
+
+ context 'when DB is read-only' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'returns a users relation' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ expect(merge_request.assignees).to be_a(ActiveRecord::Relation)
+ expect(merge_request.assignees).to eq([user])
+ end
+
+ it 'returns an empty relation if no assignee_id is set' do
+ merge_request = create(:merge_request, assignee_id: nil)
+
+ expect(merge_request.assignees).to be_a(ActiveRecord::Relation)
+ expect(merge_request.assignees).to eq([])
+ end
+ end
+ end
+
+ describe '#assignee_ids' do
+ context 'when assignee_id exists and there is no relation' do
+ it 'creates the relation' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignee_ids }.to change { merge_request.merge_request_assignees.count }.from(0).to(1)
+ end
+
+ it 'nullifies the assignee_id' do
+ merge_request = create(:merge_request, assignee_id: nil)
+ merge_request.update_column(:assignee_id, user.id)
+
+ expect { merge_request.assignee_ids }
+ .to change { merge_request.read_attribute(:assignee_id) }
+ .from(user.id).to(nil)
+ end
+ end
+
+ context 'when DB is read-only' do
+ before do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+ end
+
+ it 'returns a list of user IDs' do
+ merge_request = create(:merge_request, assignee_id: user.id)
+
+ expect(merge_request.assignee_ids).to be_a(Array)
+ expect(merge_request.assignee_ids).to eq([user.id])
+ end
+
+ it 'returns an empty relation if no assignee_id is set' do
+ merge_request = create(:merge_request, assignee_id: nil)
+
+ expect(merge_request.assignee_ids).to be_a(Array)
+ expect(merge_request.assignee_ids).to eq([])
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 64bf04071e8..baddca47dfa 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiscussionOnDiff do
diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb
index 17224c09693..c4cf8e80f7a 100644
--- a/spec/models/concerns/each_batch_spec.rb
+++ b/spec/models/concerns/each_batch_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EachBatch do
diff --git a/spec/models/concerns/editable_spec.rb b/spec/models/concerns/editable_spec.rb
index 49a9a8ebcbc..4a4a3ca5687 100644
--- a/spec/models/concerns/editable_spec.rb
+++ b/spec/models/concerns/editable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Editable do
diff --git a/spec/models/concerns/expirable_spec.rb b/spec/models/concerns/expirable_spec.rb
index f7b436f32e6..f4f5eab5b86 100644
--- a/spec/models/concerns/expirable_spec.rb
+++ b/spec/models/concerns/expirable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Expirable do
diff --git a/spec/models/concerns/faster_cache_keys_spec.rb b/spec/models/concerns/faster_cache_keys_spec.rb
index 8d3f94267fa..7830acbae3d 100644
--- a/spec/models/concerns/faster_cache_keys_spec.rb
+++ b/spec/models/concerns/faster_cache_keys_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe FasterCacheKeys do
diff --git a/spec/models/concerns/feature_gate_spec.rb b/spec/models/concerns/feature_gate_spec.rb
index 3f601243245..276d3d9e1d5 100644
--- a/spec/models/concerns/feature_gate_spec.rb
+++ b/spec/models/concerns/feature_gate_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe FeatureGate do
diff --git a/spec/models/concerns/group_descendant_spec.rb b/spec/models/concerns/group_descendant_spec.rb
index 28352d8c961..194caac3fce 100644
--- a/spec/models/concerns/group_descendant_spec.rb
+++ b/spec/models/concerns/group_descendant_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupDescendant, :nested_groups do
diff --git a/spec/models/concerns/has_ref_spec.rb b/spec/models/concerns/has_ref_spec.rb
index 8aed72d77a4..66b25c77430 100644
--- a/spec/models/concerns/has_ref_spec.rb
+++ b/spec/models/concerns/has_ref_spec.rb
@@ -16,6 +16,16 @@ describe HasRef do
it 'return true when tag is set to false' do
is_expected.to be_truthy
end
+
+ context 'when it was triggered by merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns false' do
+ is_expected.to be_falsy
+ end
+ end
end
context 'is not a tag' do
@@ -55,5 +65,15 @@ describe HasRef do
is_expected.to start_with(Gitlab::Git::BRANCH_REF_PREFIX)
end
end
+
+ context 'when it is triggered by a merge request' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+ let(:build) { create(:ci_build, tag: false, pipeline: pipeline) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
end
end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index 6b1038cb8fd..a217dc42537 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe HasStatus do
@@ -34,6 +36,22 @@ describe HasStatus do
it { is_expected.to eq 'running' }
end
+ context 'all preparing' do
+ let!(:statuses) do
+ [create(type, status: :preparing), create(type, status: :preparing)]
+ end
+
+ it { is_expected.to eq 'preparing' }
+ end
+
+ context 'at least one preparing' do
+ let!(:statuses) do
+ [create(type, status: :success), create(type, status: :preparing)]
+ end
+
+ it { is_expected.to eq 'preparing' }
+ end
+
context 'success and failed but allowed to fail' do
let!(:statuses) do
[create(type, status: :success),
@@ -188,7 +206,7 @@ describe HasStatus do
end
end
- %i[created running pending success
+ %i[created preparing running pending success
failed canceled skipped].each do |status|
it_behaves_like 'having a job', status
end
@@ -234,7 +252,7 @@ describe HasStatus do
describe '.alive' do
subject { CommitStatus.alive }
- %i[running pending created].each do |status|
+ %i[running pending preparing created].each do |status|
it_behaves_like 'containing the job', status
end
@@ -270,7 +288,7 @@ describe HasStatus do
describe '.cancelable' do
subject { CommitStatus.cancelable }
- %i[running pending created scheduled].each do |status|
+ %i[running pending preparing created scheduled].each do |status|
it_behaves_like 'containing the job', status
end
diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/has_variable_spec.rb
index 3fbe86c5b56..2bb21d7934e 100644
--- a/spec/models/concerns/has_variable_spec.rb
+++ b/spec/models/concerns/has_variable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe HasVariable do
@@ -57,7 +59,7 @@ describe HasVariable do
describe '#to_runner_variable' do
it 'returns a hash for the runner' do
expect(subject.to_runner_variable)
- .to eq(key: subject.key, value: subject.value, public: false)
+ .to include(key: subject.key, value: subject.value, public: false)
end
end
end
diff --git a/spec/models/concerns/ignorable_column_spec.rb b/spec/models/concerns/ignorable_column_spec.rb
index b70f2331a0e..6b82825d2cc 100644
--- a/spec/models/concerns/ignorable_column_spec.rb
+++ b/spec/models/concerns/ignorable_column_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IgnorableColumn do
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 41159348e04..64f02978d79 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issuable do
@@ -32,17 +34,56 @@ describe Issuable do
end
describe "Validation" do
- subject { build(:issue) }
+ context 'general validations' do
+ subject { build(:issue) }
- before do
- allow(InternalId).to receive(:generate_next).and_return(nil)
+ before do
+ allow(InternalId).to receive(:generate_next).and_return(nil)
+ end
+
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:iid) }
+ it { is_expected.to validate_presence_of(:author) }
+ it { is_expected.to validate_presence_of(:title) }
+ it { is_expected.to validate_length_of(:title).is_at_most(255) }
end
- it { is_expected.to validate_presence_of(:project) }
- it { is_expected.to validate_presence_of(:iid) }
- it { is_expected.to validate_presence_of(:author) }
- it { is_expected.to validate_presence_of(:title) }
- it { is_expected.to validate_length_of(:title).is_at_most(255) }
+ describe 'milestone' do
+ let(:project) { create(:project) }
+ let(:milestone_id) { create(:milestone, project: project).id }
+ let(:params) do
+ {
+ title: 'something',
+ project: project,
+ author: build(:user),
+ milestone_id: milestone_id
+ }
+ end
+
+ subject { issuable_class.new(params) }
+
+ context 'with correct params' do
+ it { is_expected.to be_valid }
+ end
+
+ context 'with empty string milestone' do
+ let(:milestone_id) { '' }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with nil milestone id' do
+ let(:milestone_id) { nil }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'with a milestone id from another project' do
+ let(:milestone_id) { create(:milestone).id }
+
+ it { is_expected.to be_invalid }
+ end
+ end
end
describe "Scope" do
@@ -66,6 +107,48 @@ describe Issuable do
end
end
+ describe '#milestone_available?' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:issue) { create(:issue, project: project) }
+
+ def build_issuable(milestone_id)
+ issuable_class.new(project: project, milestone_id: milestone_id)
+ end
+
+ it 'returns true with a milestone from the issue project' do
+ milestone = create(:milestone, project: project)
+
+ expect(build_issuable(milestone.id).milestone_available?).to be_truthy
+ end
+
+ it 'returns true with a milestone from the issue project group' do
+ milestone = create(:milestone, group: group)
+
+ expect(build_issuable(milestone.id).milestone_available?).to be_truthy
+ end
+
+ it 'returns true with a milestone from the the parent of the issue project group', :nested_groups do
+ parent = create(:group)
+ group.update(parent: parent)
+ milestone = create(:milestone, group: parent)
+
+ expect(build_issuable(milestone.id).milestone_available?).to be_truthy
+ end
+
+ it 'returns false with a milestone from another project' do
+ milestone = create(:milestone)
+
+ expect(build_issuable(milestone.id).milestone_available?).to be_falsey
+ end
+
+ it 'returns false with a milestone from another group' do
+ milestone = create(:milestone, group: create(:group))
+
+ expect(build_issuable(milestone.id).milestone_available?).to be_falsey
+ end
+ end
+
describe ".search" do
let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") }
let!(:searchable_issue2) { create(:issue, title: 'Aw') }
@@ -419,8 +502,8 @@ describe Issuable do
let(:user2) { create(:user) }
before do
- merge_request.update(assignee: user)
- merge_request.update(assignee: user2)
+ merge_request.update(assignees: [user])
+ merge_request.update(assignees: [user, user2])
expect(Gitlab::HookData::IssuableBuilder)
.to receive(:new).with(merge_request).and_return(builder)
end
@@ -429,8 +512,7 @@ describe Issuable do
expect(builder).to receive(:build).with(
user: user,
changes: hash_including(
- 'assignee_id' => [user.id, user2.id],
- 'assignee' => [user.hook_attrs, user2.hook_attrs]
+ 'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]]
))
merge_request.to_hook_data(user, old_associations: { assignees: [user] })
@@ -576,7 +658,7 @@ describe Issuable do
end
context 'adding time' do
- it 'should update the total time spent' do
+ it 'updates the total time spent' do
spend_time(1800)
expect(issue.total_time_spent).to eq(1800)
@@ -596,7 +678,7 @@ describe Issuable do
spend_time(1800)
end
- it 'should update the total time spent' do
+ it 'updates the total time spent' do
spend_time(-900)
expect(issue.total_time_spent).to eq(900)
diff --git a/spec/models/concerns/issuable_states_spec.rb b/spec/models/concerns/issuable_states_spec.rb
new file mode 100644
index 00000000000..70450159cc0
--- /dev/null
+++ b/spec/models/concerns/issuable_states_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# This spec checks if state_id column of issues and merge requests
+# are being synced on every save.
+# It can be removed in the next release. Check https://gitlab.com/gitlab-org/gitlab-ce/issues/51789 for more information.
+describe IssuableStates do
+ [Issue, MergeRequest].each do |klass|
+ it "syncs state_id column when #{klass.model_name.human} gets created" do
+ klass.available_states.each do |state, state_id|
+ issuable = build(klass.model_name.param_key, state: state.to_s)
+
+ issuable.save!
+
+ expect(issuable.state_id).to eq(state_id)
+ end
+ end
+
+ it "syncs state_id column when #{klass.model_name.human} gets updated" do
+ klass.available_states.each do |state, state_id|
+ issuable = create(klass.model_name.param_key, state: state.to_s)
+
+ issuable.update(state: state)
+
+ expect(issuable.state_id).to eq(state_id)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/loaded_in_group_list_spec.rb b/spec/models/concerns/loaded_in_group_list_spec.rb
index 7a279547a3a..7c97b580779 100644
--- a/spec/models/concerns/loaded_in_group_list_spec.rb
+++ b/spec/models/concerns/loaded_in_group_list_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LoadedInGroupList do
diff --git a/spec/models/concerns/manual_inverse_association_spec.rb b/spec/models/concerns/manual_inverse_association_spec.rb
index ff4a04ea573..ee32e3b165b 100644
--- a/spec/models/concerns/manual_inverse_association_spec.rb
+++ b/spec/models/concerns/manual_inverse_association_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ManualInverseAssociation do
diff --git a/spec/models/concerns/maskable_spec.rb b/spec/models/concerns/maskable_spec.rb
new file mode 100644
index 00000000000..aeba7ad862f
--- /dev/null
+++ b/spec/models/concerns/maskable_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Maskable do
+ let(:variable) { build(:ci_variable) }
+
+ describe 'masked value validations' do
+ subject { variable }
+
+ context 'when variable is masked' do
+ before do
+ subject.masked = true
+ end
+
+ it { is_expected.not_to allow_value('hello').for(:value) }
+ it { is_expected.not_to allow_value('hello world').for(:value) }
+ it { is_expected.not_to allow_value('hello$VARIABLEworld').for(:value) }
+ it { is_expected.not_to allow_value('hello\rworld').for(:value) }
+ it { is_expected.to allow_value('helloworld').for(:value) }
+ end
+
+ context 'when variable is not masked' do
+ before do
+ subject.masked = false
+ end
+
+ it { is_expected.to allow_value('hello').for(:value) }
+ it { is_expected.to allow_value('hello world').for(:value) }
+ it { is_expected.to allow_value('hello$VARIABLEworld').for(:value) }
+ it { is_expected.to allow_value('hello\rworld').for(:value) }
+ it { is_expected.to allow_value('helloworld').for(:value) }
+ end
+ end
+
+ describe 'REGEX' do
+ subject { Maskable::REGEX }
+
+ it 'does not match strings shorter than 8 letters' do
+ expect(subject.match?('hello')).to eq(false)
+ end
+
+ it 'does not match strings with spaces' do
+ expect(subject.match?('hello world')).to eq(false)
+ end
+
+ it 'does not match strings with shell variables' do
+ expect(subject.match?('hello$VARIABLEworld')).to eq(false)
+ end
+
+ it 'does not match strings with escape characters' do
+ expect(subject.match?('hello\rworld')).to eq(false)
+ end
+
+ it 'does not match strings that span more than one line' do
+ string = <<~EOS
+ hello
+ world
+ EOS
+
+ expect(subject.match?(string)).to eq(false)
+ end
+
+ it 'matches valid strings' do
+ expect(subject.match?('helloworld')).to eq(true)
+ end
+ end
+
+ describe '#to_runner_variable' do
+ subject { variable.to_runner_variable }
+
+ it 'exposes the masked attribute' do
+ expect(subject).to include(:masked)
+ end
+ end
+end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index a9b237fa9ea..f31e3e8821d 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Mentionable do
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 87bf731340f..7e9a8306612 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Milestone, 'Milestoneish' do
@@ -9,8 +11,10 @@ describe Milestone, 'Milestoneish' do
let(:admin) { create(:admin) }
let(:project) { create(:project, :public) }
let(:milestone) { create(:milestone, project: project) }
- let!(:issue) { create(:issue, project: project, milestone: milestone) }
- let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) }
+ let(:label1) { create(:label, project: project) }
+ let(:label2) { create(:label, project: project) }
+ let!(:issue) { create(:issue, project: project, milestone: milestone, assignees: [member], labels: [label1]) }
+ let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone, labels: [label2]) }
let!(:security_issue_2) { create(:issue, :confidential, project: project, assignees: [assignee], milestone: milestone) }
let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) }
let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) }
@@ -42,69 +46,151 @@ describe Milestone, 'Milestoneish' do
end
end
- describe '#sorted_merge_requests' do
- it 'sorts merge requests by label priority' do
- merge_request_1 = create(:labeled_merge_request, labels: [label_2], source_project: project, source_branch: 'branch_1', milestone: milestone)
- merge_request_2 = create(:labeled_merge_request, labels: [label_1], source_project: project, source_branch: 'branch_2', milestone: milestone)
- merge_request_3 = create(:labeled_merge_request, labels: [label_3], source_project: project, source_branch: 'branch_3', milestone: milestone)
-
- merge_requests = milestone.sorted_merge_requests
-
- expect(merge_requests.first).to eq(merge_request_2)
- expect(merge_requests.second).to eq(merge_request_1)
- expect(merge_requests.third).to eq(merge_request_3)
+ context 'attributes visibility' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:users) do
+ {
+ anonymous: nil,
+ non_member: non_member,
+ guest: guest,
+ member: member,
+ assignee: assignee
+ }
end
- end
- describe '#closed_items_count' do
- it 'does not count confidential issues for non project members' do
- expect(milestone.closed_items_count(non_member)).to eq 2
+ let(:project_visibility_levels) do
+ {
+ public: Gitlab::VisibilityLevel::PUBLIC,
+ internal: Gitlab::VisibilityLevel::INTERNAL,
+ private: Gitlab::VisibilityLevel::PRIVATE
+ }
end
- it 'does not count confidential issues for project members with guest role' do
- expect(milestone.closed_items_count(guest)).to eq 2
+ describe '#issue_participants_visible_by_user' do
+ where(:visibility, :user_role, :result) do
+ :public | nil | [:member]
+ :public | :non_member | [:member]
+ :public | :guest | [:member]
+ :public | :member | [:member, :assignee]
+ :internal | nil | []
+ :internal | :non_member | [:member]
+ :internal | :guest | [:member]
+ :internal | :member | [:member, :assignee]
+ :private | nil | []
+ :private | :non_member | []
+ :private | :guest | [:member]
+ :private | :member | [:member, :assignee]
+ end
+
+ with_them do
+ before do
+ project.update(visibility_level: project_visibility_levels[visibility])
+ end
+
+ it 'returns the proper participants' do
+ user = users[user_role]
+ participants = result.map { |role| users[role] }
+
+ expect(milestone.issue_participants_visible_by_user(user)).to match_array(participants)
+ end
+ end
end
- it 'counts confidential issues for author' do
- expect(milestone.closed_items_count(author)).to eq 4
+ describe '#issue_labels_visible_by_user' do
+ let(:labels) do
+ {
+ label1: label1,
+ label2: label2
+ }
+ end
+
+ where(:visibility, :user_role, :result) do
+ :public | nil | [:label1]
+ :public | :non_member | [:label1]
+ :public | :guest | [:label1]
+ :public | :member | [:label1, :label2]
+ :internal | nil | []
+ :internal | :non_member | [:label1]
+ :internal | :guest | [:label1]
+ :internal | :member | [:label1, :label2]
+ :private | nil | []
+ :private | :non_member | []
+ :private | :guest | [:label1]
+ :private | :member | [:label1, :label2]
+ end
+
+ with_them do
+ before do
+ project.update(visibility_level: project_visibility_levels[visibility])
+ end
+
+ it 'returns the proper participants' do
+ user = users[user_role]
+ expected_labels = result.map { |label| labels[label] }
+
+ expect(milestone.issue_labels_visible_by_user(user)).to match_array(expected_labels)
+ end
+ end
end
+ end
- it 'counts confidential issues for assignee' do
- expect(milestone.closed_items_count(assignee)).to eq 4
- end
+ describe '#sorted_merge_requests' do
+ it 'sorts merge requests by label priority' do
+ merge_request_1 = create(:labeled_merge_request, labels: [label_2], source_project: project, source_branch: 'branch_1', milestone: milestone)
+ merge_request_2 = create(:labeled_merge_request, labels: [label_1], source_project: project, source_branch: 'branch_2', milestone: milestone)
+ merge_request_3 = create(:labeled_merge_request, labels: [label_3], source_project: project, source_branch: 'branch_3', milestone: milestone)
- it 'counts confidential issues for project members' do
- expect(milestone.closed_items_count(member)).to eq 6
- end
+ merge_requests = milestone.sorted_merge_requests(member)
- it 'counts all issues for admin' do
- expect(milestone.closed_items_count(admin)).to eq 6
+ expect(merge_requests.first).to eq(merge_request_2)
+ expect(merge_requests.second).to eq(merge_request_1)
+ expect(merge_requests.third).to eq(merge_request_3)
end
end
- describe '#total_items_count' do
- it 'does not count confidential issues for non project members' do
- expect(milestone.total_items_count(non_member)).to eq 4
- end
+ describe '#merge_requests_visible_to_user' do
+ let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) }
- it 'does not count confidential issues for project members with guest role' do
- expect(milestone.total_items_count(guest)).to eq 4
- end
+ context 'when project is private' do
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
- it 'counts confidential issues for author' do
- expect(milestone.total_items_count(author)).to eq 7
- end
-
- it 'counts confidential issues for assignee' do
- expect(milestone.total_items_count(assignee)).to eq 7
- end
+ it 'does not return any merge request for a non member' do
+ merge_requests = milestone.merge_requests_visible_to_user(non_member)
+ expect(merge_requests).to be_empty
+ end
- it 'counts confidential issues for project members' do
- expect(milestone.total_items_count(member)).to eq 10
+ it 'returns milestone merge requests for a member' do
+ merge_requests = milestone.merge_requests_visible_to_user(member)
+ expect(merge_requests).to contain_exactly(merge_request)
+ end
end
- it 'counts all issues for admin' do
- expect(milestone.total_items_count(admin)).to eq 10
+ context 'when project is public' do
+ context 'when merge requests are available to anyone' do
+ it 'returns milestone merge requests for a non member' do
+ merge_requests = milestone.merge_requests_visible_to_user(non_member)
+ expect(merge_requests).to contain_exactly(merge_request)
+ end
+ end
+
+ context 'when merge requests are available to project members' do
+ before do
+ project.project_feature.update(merge_requests_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'does not return any merge request for a non member' do
+ merge_requests = milestone.merge_requests_visible_to_user(non_member)
+ expect(merge_requests).to be_empty
+ end
+
+ it 'returns milestone merge requests for a member' do
+ merge_requests = milestone.merge_requests_visible_to_user(member)
+ expect(merge_requests).to contain_exactly(merge_request)
+ end
+ end
end
end
@@ -122,28 +208,42 @@ describe Milestone, 'Milestoneish' do
end
describe '#percent_complete' do
+ context 'division by zero' do
+ let(:new_milestone) { build_stubbed(:milestone) }
+
+ it { expect(new_milestone.percent_complete(admin)).to eq(0) }
+ end
+ end
+
+ describe '#count_issues_by_state' do
it 'does not count confidential issues for non project members' do
- expect(milestone.percent_complete(non_member)).to eq 50
+ expect(milestone.closed_issues_count(non_member)).to eq 2
+ expect(milestone.total_issues_count(non_member)).to eq 3
end
it 'does not count confidential issues for project members with guest role' do
- expect(milestone.percent_complete(guest)).to eq 50
+ expect(milestone.closed_issues_count(guest)).to eq 2
+ expect(milestone.total_issues_count(guest)).to eq 3
end
it 'counts confidential issues for author' do
- expect(milestone.percent_complete(author)).to eq 57
+ expect(milestone.closed_issues_count(author)).to eq 4
+ expect(milestone.total_issues_count(author)).to eq 6
end
it 'counts confidential issues for assignee' do
- expect(milestone.percent_complete(assignee)).to eq 57
+ expect(milestone.closed_issues_count(assignee)).to eq 4
+ expect(milestone.total_issues_count(assignee)).to eq 6
end
it 'counts confidential issues for project members' do
- expect(milestone.percent_complete(member)).to eq 60
+ expect(milestone.closed_issues_count(member)).to eq 6
+ expect(milestone.total_issues_count(member)).to eq 9
end
it 'counts confidential issues for admin' do
- expect(milestone.percent_complete(admin)).to eq 60
+ expect(milestone.closed_issues_count(admin)).to eq 6
+ expect(milestone.total_issues_count(admin)).to eq 9
end
end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 485a6e165a1..e17b98536fa 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Noteable do
@@ -258,4 +260,16 @@ describe Noteable do
end
end
end
+
+ describe '.replyable_types' do
+ it 'exposes the replyable types' do
+ expect(described_class.replyable_types).to include('Issue', 'MergeRequest')
+ end
+ end
+
+ describe '.resolvable_types' do
+ it 'exposes the replyable types' do
+ expect(described_class.resolvable_types).to include('MergeRequest')
+ end
+ end
end
diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb
index 431f1482615..3d5937c4fc6 100644
--- a/spec/models/concerns/participable_spec.rb
+++ b/spec/models/concerns/participable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Participable do
diff --git a/spec/models/concerns/presentable_spec.rb b/spec/models/concerns/presentable_spec.rb
index 941647a79fb..9db868dd348 100644
--- a/spec/models/concerns/presentable_spec.rb
+++ b/spec/models/concerns/presentable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Presentable do
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index 9041690023f..5aa43b58217 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectFeaturesCompatibility do
diff --git a/spec/models/concerns/prometheus_adapter_spec.rb b/spec/models/concerns/prometheus_adapter_spec.rb
index f4b9c57e71a..25a2d290f76 100644
--- a/spec/models/concerns/prometheus_adapter_spec.rb
+++ b/spec/models/concerns/prometheus_adapter_spec.rb
@@ -1,17 +1,20 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
include PrometheusHelpers
include ReactiveCachingHelpers
- class TestClass
- include PrometheusAdapter
- end
-
let(:project) { create(:prometheus_project) }
let(:service) { project.prometheus_service }
- let(:described_class) { TestClass }
+ let(:described_class) do
+ Class.new do
+ include PrometheusAdapter
+ end
+ end
+
let(:environment_query) { Gitlab::Prometheus::Queries::EnvironmentQuery }
describe '#query' do
@@ -74,6 +77,28 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe 'additional_metrics' do
+ let(:additional_metrics_environment_query) { Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery }
+ let(:environment) { build_stubbed(:environment, slug: 'env-slug') }
+ let(:time_window) { [1552642245.067, 1552642095.831] }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ context 'with valid data' do
+ subject { service.query(:additional_metrics_environment, environment, *time_window) }
+
+ before do
+ stub_reactive_cache(service, prometheus_data, additional_metrics_environment_query, environment.id, *time_window)
+ end
+
+ it 'returns reactive data' do
+ expect(subject).to eq(prometheus_data)
+ end
+ end
+ end
end
describe '#calculate_reactive_cache' do
@@ -118,4 +143,24 @@ describe PrometheusAdapter, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '#build_query_args' do
+ subject { service.build_query_args(*args) }
+
+ context 'when active record models are included' do
+ let(:args) { [double(:environment, id: 12)] }
+
+ it 'serializes by id' do
+ is_expected.to eq [12]
+ end
+ end
+
+ context 'when args are safe for serialization' do
+ let(:args) { ['stringy arg', 5, 6.0, :symbolic_arg] }
+
+ it 'does nothing' do
+ is_expected.to eq args
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/protected_ref_access_spec.rb b/spec/models/concerns/protected_ref_access_spec.rb
index ce602337647..f63ad958ed3 100644
--- a/spec/models/concerns/protected_ref_access_spec.rb
+++ b/spec/models/concerns/protected_ref_access_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedRefAccess do
+ include ExternalAuthorizationServiceHelpers
+
subject(:protected_ref_access) do
create(:protected_branch, :maintainers_can_push).push_access_levels.first
end
@@ -27,5 +31,15 @@ describe ProtectedRefAccess do
expect(protected_ref_access.check_access(developer)).to be_falsy
end
+
+ context 'external authorization' do
+ it 'is false if external authorization denies access' do
+ maintainer = create(:user)
+ project.add_maintainer(maintainer)
+ external_service_deny_access(maintainer, project)
+
+ expect(protected_ref_access.check_access(maintainer)).to be_falsey
+ end
+ end
end
end
diff --git a/spec/models/concerns/reactive_caching_spec.rb b/spec/models/concerns/reactive_caching_spec.rb
index 03ae45e6b17..53df9e0bc05 100644
--- a/spec/models/concerns/reactive_caching_spec.rb
+++ b/spec/models/concerns/reactive_caching_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ReactiveCaching, :use_clean_rails_memory_store_caching do
@@ -14,6 +16,10 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
attr_reader :id
+ def self.primary_key
+ :id
+ end
+
def initialize(id, &blk)
@id = id
@calculator = blk
@@ -104,6 +110,46 @@ describe ReactiveCaching, :use_clean_rails_memory_store_caching do
end
end
+ describe '.reactive_cache_worker_finder' do
+ context 'with default reactive_cache_worker_finder' do
+ let(:args) { %w(other args) }
+
+ before do
+ allow(instance.class).to receive(:find_by).with(id: instance.id)
+ .and_return(instance)
+ end
+
+ it 'calls the activerecord find_by method' do
+ result = instance.class.reactive_cache_worker_finder.call(instance.id, *args)
+
+ expect(result).to eq(instance)
+ expect(instance.class).to have_received(:find_by).with(id: instance.id)
+ end
+ end
+
+ context 'with custom reactive_cache_worker_finder' do
+ let(:args) { %w(arg1 arg2) }
+ let(:instance) { CustomFinderCacheTest.new(666, &calculation) }
+
+ class CustomFinderCacheTest < CacheTest
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+ def self.from_cache(*args); end
+ end
+
+ before do
+ allow(instance.class).to receive(:from_cache).with(*args).and_return(instance)
+ end
+
+ it 'overrides the default reactive_cache_worker_finder' do
+ result = instance.class.reactive_cache_worker_finder.call(instance.id, *args)
+
+ expect(result).to eq(instance)
+ expect(instance.class).to have_received(:from_cache).with(*args)
+ end
+ end
+ end
+
describe '#clear_reactive_cache!' do
before do
stub_reactive_cache(instance, 4)
diff --git a/spec/models/concerns/redactable_spec.rb b/spec/models/concerns/redactable_spec.rb
index 7feeaa54069..57c7d2cb767 100644
--- a/spec/models/concerns/redactable_spec.rb
+++ b/spec/models/concerns/redactable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Redactable do
diff --git a/spec/models/concerns/redis_cacheable_spec.rb b/spec/models/concerns/redis_cacheable_spec.rb
index 23c6c6233e9..a9dca27f258 100644
--- a/spec/models/concerns/redis_cacheable_spec.rb
+++ b/spec/models/concerns/redis_cacheable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RedisCacheable do
diff --git a/spec/models/concerns/relative_positioning_spec.rb b/spec/models/concerns/relative_positioning_spec.rb
index ac8da30b6c9..d0ae45f7871 100644
--- a/spec/models/concerns/relative_positioning_spec.rb
+++ b/spec/models/concerns/relative_positioning_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RelativePositioning do
diff --git a/spec/models/concerns/resolvable_discussion_spec.rb b/spec/models/concerns/resolvable_discussion_spec.rb
index 97b046b0f21..9ea01ca9002 100644
--- a/spec/models/concerns/resolvable_discussion_spec.rb
+++ b/spec/models/concerns/resolvable_discussion_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Discussion, ResolvableDiscussion do
diff --git a/spec/models/concerns/resolvable_note_spec.rb b/spec/models/concerns/resolvable_note_spec.rb
index fcb5250278e..4f46252a044 100644
--- a/spec/models/concerns/resolvable_note_spec.rb
+++ b/spec/models/concerns/resolvable_note_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Note, ResolvableNote do
diff --git a/spec/models/concerns/routable_spec.rb b/spec/models/concerns/routable_spec.rb
index 565266321d3..1fb0dd5030c 100644
--- a/spec/models/concerns/routable_spec.rb
+++ b/spec/models/concerns/routable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Group, 'Routable' do
diff --git a/spec/models/concerns/sha_attribute_spec.rb b/spec/models/concerns/sha_attribute_spec.rb
index 0d3beb6a6e3..a4a81ae126d 100644
--- a/spec/models/concerns/sha_attribute_spec.rb
+++ b/spec/models/concerns/sha_attribute_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ShaAttribute do
diff --git a/spec/models/concerns/sortable_spec.rb b/spec/models/concerns/sortable_spec.rb
index 39c16ae60af..184f7986a6f 100644
--- a/spec/models/concerns/sortable_spec.rb
+++ b/spec/models/concerns/sortable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Sortable do
@@ -99,7 +101,7 @@ describe Sortable do
expect(ordered_group_names('id_desc')).to eq(%w(bbb BB AAA aa))
end
- it 'sorts groups by name via case-insentitive comparision' do
+ it 'sorts groups by name via case-insensitive comparision' do
expect(ordered_group_names('name_asc')).to eq(%w(aa AAA BB bbb))
expect(ordered_group_names('name_desc')).to eq(%w(bbb BB AAA aa))
end
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index e698207166c..67353475251 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Spammable do
@@ -10,7 +12,7 @@ describe Spammable do
end
describe 'ClassMethods' do
- it 'should return correct attr_spammable' do
+ it 'returns correct attr_spammable' do
expect(issue.spammable_text).to eq("#{issue.title}\n#{issue.description}")
end
end
@@ -18,7 +20,7 @@ describe Spammable do
describe 'InstanceMethods' do
let(:issue) { build(:issue, spam: true) }
- it 'should be invalid if spam' do
+ it 'is invalid if spam' do
expect(issue.valid?).to be_falsey
end
diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb
index 8c945686b66..5c0d1042e06 100644
--- a/spec/models/concerns/strip_attribute_spec.rb
+++ b/spec/models/concerns/strip_attribute_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe StripAttribute do
diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb
index 45dfb136aea..2f88adf08dd 100644
--- a/spec/models/concerns/subscribable_spec.rb
+++ b/spec/models/concerns/subscribable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Subscribable, 'Subscribable' do
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index 40cb4eef60a..51e28974ae0 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
shared_examples 'TokenAuthenticatable' do
diff --git a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
index 6605f1f5a5f..7332da309d5 100644
--- a/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/base_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TokenAuthenticatableStrategies::Base do
@@ -15,7 +17,7 @@ describe TokenAuthenticatableStrategies::Base do
context 'when encrypted strategy is specified' do
it 'fabricates encrypted strategy object' do
- strategy = described_class.fabricate(instance, field, encrypted: true)
+ strategy = described_class.fabricate(instance, field, encrypted: :required)
expect(strategy).to be_a TokenAuthenticatableStrategies::Encrypted
end
@@ -23,7 +25,7 @@ describe TokenAuthenticatableStrategies::Base do
context 'when no strategy is specified' do
it 'fabricates insecure strategy object' do
- strategy = described_class.fabricate(instance, field, something: true)
+ strategy = described_class.fabricate(instance, field, something: :required)
expect(strategy).to be_a TokenAuthenticatableStrategies::Insecure
end
@@ -31,35 +33,9 @@ describe TokenAuthenticatableStrategies::Base do
context 'when incompatible options are provided' do
it 'raises an error' do
- expect { described_class.fabricate(instance, field, digest: true, encrypted: true) }
+ expect { described_class.fabricate(instance, field, digest: true, encrypted: :required) }
.to raise_error ArgumentError
end
end
end
-
- describe '#fallback?' do
- context 'when fallback is set' do
- it 'recognizes fallback setting' do
- strategy = described_class.new(instance, field, fallback: true)
-
- expect(strategy.fallback?).to be true
- end
- end
-
- context 'when fallback is not a valid value' do
- it 'raises an error' do
- strategy = described_class.new(instance, field, fallback: 'something')
-
- expect { strategy.fallback? }.to raise_error ArgumentError
- end
- end
-
- context 'when fallback is not set' do
- it 'raises an error' do
- strategy = described_class.new(instance, field, {})
-
- expect(strategy.fallback?).to eq false
- end
- end
- end
end
diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
index 93cab80cb1f..70f41981b3b 100644
--- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
+++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TokenAuthenticatableStrategies::Encrypted do
@@ -12,19 +14,9 @@ describe TokenAuthenticatableStrategies::Encrypted do
described_class.new(model, 'some_field', options)
end
- describe '.new' do
- context 'when fallback and migration strategies are set' do
- let(:options) { { fallback: true, migrating: true } }
-
- it 'raises an error' do
- expect { subject }.to raise_error ArgumentError, /not compatible/
- end
- end
- end
-
describe '#find_token_authenticatable' do
- context 'when using fallback strategy' do
- let(:options) { { fallback: true } }
+ context 'when using optional strategy' do
+ let(:options) { { encrypted: :optional } }
it 'finds the encrypted resource by cleartext' do
allow(model).to receive(:find_by)
@@ -50,7 +42,7 @@ describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when using migration strategy' do
- let(:options) { { migrating: true } }
+ let(:options) { { encrypted: :migrating } }
it 'finds the cleartext resource by cleartext' do
allow(model).to receive(:find_by)
@@ -73,8 +65,8 @@ describe TokenAuthenticatableStrategies::Encrypted do
end
describe '#get_token' do
- context 'when using fallback strategy' do
- let(:options) { { fallback: true } }
+ context 'when using optional strategy' do
+ let(:options) { { encrypted: :optional } }
it 'returns decrypted token when an encrypted token is present' do
allow(instance).to receive(:read_attribute)
@@ -98,7 +90,7 @@ describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when using migration strategy' do
- let(:options) { { migrating: true } }
+ let(:options) { { encrypted: :migrating } }
it 'returns cleartext token when an encrypted token is present' do
allow(instance).to receive(:read_attribute)
@@ -127,8 +119,8 @@ describe TokenAuthenticatableStrategies::Encrypted do
end
describe '#set_token' do
- context 'when using fallback strategy' do
- let(:options) { { fallback: true } }
+ context 'when using optional strategy' do
+ let(:options) { { encrypted: :optional } }
it 'writes encrypted token and removes plaintext token and returns it' do
expect(instance).to receive(:[]=)
@@ -141,7 +133,7 @@ describe TokenAuthenticatableStrategies::Encrypted do
end
context 'when using migration strategy' do
- let(:options) { { migrating: true } }
+ let(:options) { { encrypted: :migrating } }
it 'writes encrypted token and writes plaintext token' do
expect(instance).to receive(:[]=)
diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb
index 265abd6bd72..f28e5f56411 100644
--- a/spec/models/concerns/triggerable_hooks_spec.rb
+++ b/spec/models/concerns/triggerable_hooks_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe TriggerableHooks do
diff --git a/spec/models/concerns/uniquify_spec.rb b/spec/models/concerns/uniquify_spec.rb
index 6cd2de6dcce..9ba35702ba6 100644
--- a/spec/models/concerns/uniquify_spec.rb
+++ b/spec/models/concerns/uniquify_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Uniquify do
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index e46945e301e..013112d1d51 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ContainerRepository do
diff --git a/spec/models/conversational_development_index/metric_spec.rb b/spec/models/conversational_development_index/metric_spec.rb
index b3193619503..60b1a860dfd 100644
--- a/spec/models/conversational_development_index/metric_spec.rb
+++ b/spec/models/conversational_development_index/metric_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe ConversationalDevelopmentIndex::Metric do
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index 6a6b58fb52b..b22a0340015 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'CycleAnalytics#code' do
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 45f1b4fe8a3..07d60be091a 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'CycleAnalytics#issue' do
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index d366e2b723a..3d22a284264 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'CycleAnalytics#plan' do
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 156eb96cfce..383727cd8f7 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'CycleAnalytics#production' do
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 0aedfb49cb5..1af5f9cc1f4 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'CycleAnalytics#review' do
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 0cbda50c688..8375944f03c 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'CycleAnalytics#staging' do
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index e58b8fdff58..b78258df564 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'CycleAnalytics#test' do
diff --git a/spec/models/cycle_analytics_spec.rb b/spec/models/cycle_analytics_spec.rb
index 0fe24870f02..5d8b5b573cf 100644
--- a/spec/models/cycle_analytics_spec.rb
+++ b/spec/models/cycle_analytics_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CycleAnalytics do
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 41440c6d288..ec6cfb6b826 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeployKey, :mailer do
diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb
index fca3090ff4a..c137444763b 100644
--- a/spec/models/deploy_keys_project_spec.rb
+++ b/spec/models/deploy_keys_project_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeployKeysProject do
diff --git a/spec/models/deploy_token_spec.rb b/spec/models/deploy_token_spec.rb
index 3435f93c999..2fe82eaa778 100644
--- a/spec/models/deploy_token_spec.rb
+++ b/spec/models/deploy_token_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeployToken do
@@ -7,7 +9,7 @@ describe DeployToken do
it { is_expected.to have_many(:projects).through(:project_deploy_tokens) }
describe '#ensure_token' do
- it 'should ensure a token' do
+ it 'ensures a token' do
deploy_token.token = nil
deploy_token.save
@@ -17,13 +19,13 @@ describe DeployToken do
describe '#ensure_at_least_one_scope' do
context 'with at least one scope' do
- it 'should be valid' do
+ it 'is valid' do
is_expected.to be_valid
end
end
context 'with no scopes' do
- it 'should be invalid' do
+ it 'is invalid' do
deploy_token = build(:deploy_token, read_repository: false, read_registry: false)
expect(deploy_token).not_to be_valid
@@ -34,13 +36,13 @@ describe DeployToken do
describe '#scopes' do
context 'with all the scopes' do
- it 'should return scopes assigned to DeployToken' do
+ it 'returns scopes assigned to DeployToken' do
expect(deploy_token.scopes).to eq([:read_repository, :read_registry])
end
end
context 'with only one scope' do
- it 'should return scopes assigned to DeployToken' do
+ it 'returns scopes assigned to DeployToken' do
deploy_token = create(:deploy_token, read_registry: false)
expect(deploy_token.scopes).to eq([:read_repository])
end
@@ -48,7 +50,7 @@ describe DeployToken do
end
describe '#revoke!' do
- it 'should update revoke attribute' do
+ it 'updates revoke attribute' do
deploy_token.revoke!
expect(deploy_token.revoked?).to be_truthy
end
@@ -56,20 +58,20 @@ describe DeployToken do
describe "#active?" do
context "when it has been revoked" do
- it 'should return false' do
+ it 'returns false' do
deploy_token.revoke!
expect(deploy_token.active?).to be_falsy
end
end
context "when it hasn't been revoked and is not expired" do
- it 'should return true' do
+ it 'returns true' do
expect(deploy_token.active?).to be_truthy
end
end
context "when it hasn't been revoked and is expired" do
- it 'should return true' do
+ it 'returns true' do
deploy_token.update_attribute(:expires_at, Date.today - 5.days)
expect(deploy_token.active?).to be_falsy
end
@@ -78,7 +80,7 @@ describe DeployToken do
context "when it hasn't been revoked and has no expiry" do
let(:deploy_token) { create(:deploy_token, expires_at: nil) }
- it 'should return true' do
+ it 'returns true' do
expect(deploy_token.active?).to be_truthy
end
end
@@ -124,7 +126,7 @@ describe DeployToken do
context 'when using Forever.date' do
let(:deploy_token) { create(:deploy_token, expires_at: nil) }
- it 'should return nil' do
+ it 'returns nil' do
expect(deploy_token.expires_at).to be_nil
end
end
@@ -133,7 +135,7 @@ describe DeployToken do
let(:expires_at) { Date.today + 5.months }
let(:deploy_token) { create(:deploy_token, expires_at: expires_at) }
- it 'should return the personalized date' do
+ it 'returns the personalized date' do
expect(deploy_token.expires_at).to eq(expires_at)
end
end
@@ -143,7 +145,7 @@ describe DeployToken do
context 'when passing nil' do
let(:deploy_token) { create(:deploy_token, expires_at: nil) }
- it 'should assign Forever.date' do
+ it 'assigns Forever.date' do
expect(deploy_token.read_attribute(:expires_at)).to eq(Forever.date)
end
end
@@ -152,7 +154,7 @@ describe DeployToken do
let(:expires_at) { Date.today + 5.months }
let(:deploy_token) { create(:deploy_token, expires_at: expires_at) }
- it 'should respect the value' do
+ it 'respects the value' do
expect(deploy_token.read_attribute(:expires_at)).to eq(expires_at)
end
end
@@ -164,14 +166,14 @@ describe DeployToken do
subject { project.deploy_tokens.gitlab_deploy_token }
context 'with a gitlab deploy token associated' do
- it 'should return the gitlab deploy token' do
+ it 'returns the gitlab deploy token' do
deploy_token = create(:deploy_token, :gitlab_deploy_token, projects: [project])
is_expected.to eq(deploy_token)
end
end
context 'with no gitlab deploy token associated' do
- it 'should return nil' do
+ it 'returns nil' do
is_expected.to be_nil
end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index a8d53cfcd7d..1dceef3fc00 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Deployment do
subject { build(:deployment) }
- it { is_expected.to belong_to(:project) }
- it { is_expected.to belong_to(:environment) }
+ it { is_expected.to belong_to(:project).required }
+ it { is_expected.to belong_to(:environment).required }
it { is_expected.to belong_to(:user) }
it { is_expected.to belong_to(:deployable) }
@@ -100,6 +102,13 @@ describe Deployment do
deployment.succeed!
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.succeed!
+ end
end
context 'when deployment failed' do
@@ -113,6 +122,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now)
end
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.drop!
+ end
end
context 'when deployment was canceled' do
@@ -126,6 +142,13 @@ describe Deployment do
expect(deployment.finished_at).to be_like_time(Time.now)
end
end
+
+ it 'executes Deployments::FinishedWorker asynchronously' do
+ expect(Deployments::FinishedWorker)
+ .to receive(:perform_async).with(deployment.id)
+
+ deployment.cancel!
+ end
end
end
@@ -356,4 +379,38 @@ describe Deployment do
end
end
end
+
+ describe '#cluster' do
+ let(:deployment) { create(:deployment) }
+ let(:project) { deployment.project }
+ let(:environment) { deployment.environment }
+
+ subject { deployment.cluster }
+
+ before do
+ expect(project).to receive(:deployment_platform)
+ .with(environment: environment.name).and_call_original
+ end
+
+ context 'project has no deployment platform' do
+ before do
+ expect(project.clusters).to be_empty
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'project uses the kubernetes service for deployments' do
+ let!(:service) { create(:kubernetes_service, project: project) }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'project has a deployment platform' do
+ let!(:cluster) { create(:cluster, projects: [project]) }
+ let!(:platform) { create(:cluster_platform_kubernetes, cluster: cluster) }
+
+ it { is_expected.to eq cluster }
+ end
+ end
end
diff --git a/spec/models/diff_discussion_spec.rb b/spec/models/diff_discussion_spec.rb
index 50b19000799..cfeb4382927 100644
--- a/spec/models/diff_discussion_spec.rb
+++ b/spec/models/diff_discussion_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffDiscussion do
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index fda00a693f0..d9e1fe4b165 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffNote do
@@ -319,6 +321,14 @@ describe DiffNote do
end
describe '#supports_suggestion?' do
+ context 'when noteable does not exist' do
+ it 'returns false' do
+ allow(subject).to receive(:noteable) { nil }
+
+ expect(subject.supports_suggestion?).to be(false)
+ end
+ end
+
context 'when noteable does not support suggestions' do
it 'returns false' do
allow(subject.noteable).to receive(:supports_suggestion?) { false }
@@ -336,6 +346,16 @@ describe DiffNote do
end
end
+ describe '#banzai_render_context' do
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ it 'includes expected context' do
+ context = note.banzai_render_context(:note)
+
+ expect(context).to include(suggestions_filter_enabled: true, noteable: note.noteable, project: note.project)
+ end
+ end
+
describe "image diff notes" do
subject { build(:image_diff_note_on_merge_request, project: project, noteable: merge_request) }
diff --git a/spec/models/diff_viewer/base_spec.rb b/spec/models/diff_viewer/base_spec.rb
index f4efe5a7b3a..b8bdeb781dc 100644
--- a/spec/models/diff_viewer/base_spec.rb
+++ b/spec/models/diff_viewer/base_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffViewer::Base do
diff --git a/spec/models/diff_viewer/server_side_spec.rb b/spec/models/diff_viewer/server_side_spec.rb
index 86b14b6ebf3..27de0584b8a 100644
--- a/spec/models/diff_viewer/server_side_spec.rb
+++ b/spec/models/diff_viewer/server_side_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DiffViewer::ServerSide do
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index a46f7ed6507..0d02165787a 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Discussion do
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index 47eb0717c0c..cae88f39660 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Email do
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 2d554326f05..7233d2454c6 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Environment do
let(:project) { create(:project, :stubbed_repository) }
subject(:environment) { create(:environment, project: project) }
- it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:project).required }
it { is_expected.to have_many(:deployments) }
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
@@ -164,6 +166,28 @@ describe Environment do
end
end
+ describe '#name_without_type' do
+ context 'when it is inside a folder' do
+ subject(:environment) do
+ create(:environment, name: 'staging/review-1')
+ end
+
+ it 'returns name without folder' do
+ expect(environment.name_without_type).to eq 'review-1'
+ end
+ end
+
+ context 'when the environment if a top-level item itself' do
+ subject(:environment) do
+ create(:environment, name: 'production')
+ end
+
+ it 'returns full name' do
+ expect(environment.name_without_type).to eq 'production'
+ end
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
@@ -568,7 +592,7 @@ describe Environment do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
it 'returns the terminals from the deployment service' do
- expect(project.deployment_platform)
+ expect(environment.deployment_platform)
.to receive(:terminals).with(environment)
.and_return(:fake_terminals)
@@ -661,7 +685,8 @@ describe Environment do
describe '#additional_metrics' do
let(:project) { create(:prometheus_project) }
- subject { environment.additional_metrics }
+ let(:metric_params) { [] }
+ subject { environment.additional_metrics(*metric_params) }
context 'when the environment has additional metrics' do
before do
@@ -669,12 +694,26 @@ describe Environment do
end
it 'returns the additional metrics from the deployment service' do
- expect(environment.prometheus_adapter).to receive(:query)
- .with(:additional_metrics_environment, environment)
- .and_return(:fake_metrics)
+ expect(environment.prometheus_adapter)
+ .to receive(:query)
+ .with(:additional_metrics_environment, environment)
+ .and_return(:fake_metrics)
is_expected.to eq(:fake_metrics)
end
+
+ context 'when time window arguments are provided' do
+ let(:metric_params) { [1552642245.067, Time.now] }
+
+ it 'queries with the expected parameters' do
+ expect(environment.prometheus_adapter)
+ .to receive(:query)
+ .with(:additional_metrics_environment, environment, *metric_params.map(&:to_f))
+ .and_return(:fake_metrics)
+
+ is_expected.to eq(:fake_metrics)
+ end
+ end
end
context 'when the environment does not have metrics' do
diff --git a/spec/models/environment_status_spec.rb b/spec/models/environment_status_spec.rb
index 9da16dea929..c503c35305f 100644
--- a/spec/models/environment_status_spec.rb
+++ b/spec/models/environment_status_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EnvironmentStatus do
@@ -64,8 +66,8 @@ describe EnvironmentStatus do
end
describe '.for_merge_request' do
- let(:admin) { create(:admin) }
- let(:pipeline) { create(:ci_pipeline, sha: sha) }
+ let(:admin) { create(:admin) }
+ let!(:pipeline) { create(:ci_pipeline, sha: sha, merge_requests_as_head_pipeline: [merge_request]) }
it 'is based on merge_request.diff_head_sha' do
expect(merge_request).to receive(:diff_head_sha)
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index 076ccc96041..21e381d9fb7 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -62,11 +62,32 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
end
context 'URL path' do
- it 'fails validation with wrong path' do
+ it 'fails validation without api/0/projects' do
subject.api_url = 'http://gitlab.com/project1/something'
expect(subject).not_to be_valid
- expect(subject.errors.messages[:api_url]).to include('path needs to start with /api/0/projects')
+ expect(subject.errors.messages[:api_url]).to include('is invalid')
+ end
+
+ it 'fails validation without org and project slugs' do
+ subject.api_url = 'http://gitlab.com/api/0/projects/'
+
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:project]).to include('is a required field')
+ end
+
+ it 'fails validation when api_url has extra parts' do
+ subject.api_url = 'http://gitlab.com/api/0/projects/org/proj/something'
+
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:api_url]).to include("is invalid")
+ end
+
+ it 'fails validation when api_url has less parts' do
+ subject.api_url = 'http://gitlab.com/api/0/projects/org'
+
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:api_url]).to include("is invalid")
end
it 'passes validation with correct path' do
@@ -146,7 +167,7 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
- context 'when sentry client raises exception' do
+ context 'when sentry client raises Sentry::Client::Error' do
let(:sentry_client) { spy(:sentry_client) }
before do
@@ -158,7 +179,31 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
end
it 'returns error' do
- expect(result).to eq(error: 'error message')
+ expect(result).to eq(
+ error: 'error message',
+ error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
+ )
+ expect(subject).to have_received(:sentry_client)
+ expect(sentry_client).to have_received(:list_issues)
+ end
+ end
+
+ context 'when sentry client raises Sentry::Client::MissingKeysError' do
+ let(:sentry_client) { spy(:sentry_client) }
+
+ before do
+ synchronous_reactive_cache(subject)
+
+ allow(subject).to receive(:sentry_client).and_return(sentry_client)
+ allow(sentry_client).to receive(:list_issues).with(opts)
+ .and_raise(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
+ end
+
+ it 'returns error' do
+ expect(result).to eq(
+ error: 'Sentry API response is missing keys. key not found: "id"',
+ error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
+ )
expect(subject).to have_received(:sentry_client)
expect(sentry_client).to have_received(:list_issues)
end
@@ -275,6 +320,16 @@ describe ErrorTracking::ProjectErrorTrackingSetting do
expect(api_url).to eq(':::')
end
+
+ it 'returns nil when api_host is blank' do
+ api_url = described_class.build_api_url_from(
+ api_host: '',
+ organization_slug: 'org-slug',
+ project_slug: 'proj-slug'
+ )
+
+ expect(api_url).to be_nil
+ end
end
describe '#api_host' do
diff --git a/spec/models/event_collection_spec.rb b/spec/models/event_collection_spec.rb
index 6078f429bdc..efe511042c3 100644
--- a/spec/models/event_collection_spec.rb
+++ b/spec/models/event_collection_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EventCollection do
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index ce4f8ee4705..62663c247d1 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Event do
@@ -86,7 +88,7 @@ describe Event do
let(:event) { create_push_event(project, user) }
it do
- expect(event.push?).to be_truthy
+ expect(event.push_action?).to be_truthy
expect(event.visible_to_user?(user)).to be_truthy
expect(event.visible_to_user?(nil)).to be_falsey
expect(event.tag?).to be_falsey
@@ -261,7 +263,7 @@ describe Event do
context 'merge request diff note event' do
let(:project) { create(:project, :public) }
- let(:merge_request) { create(:merge_request, source_project: project, author: author, assignee: assignee) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: author, assignees: [assignee]) }
let(:note_on_merge_request) { create(:legacy_diff_note_on_merge_request, noteable: merge_request, project: project) }
let(:target) { note_on_merge_request }
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index 83ba22caa03..9d064d458f0 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ExternalIssue do
diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb
index 60d04562e6c..eab758248de 100644
--- a/spec/models/fork_network_member_spec.rb
+++ b/spec/models/fork_network_member_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ForkNetworkMember do
diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb
index a43baf1820a..5ec0f8d6b02 100644
--- a/spec/models/fork_network_spec.rb
+++ b/spec/models/fork_network_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ForkNetwork do
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index a3e68d2e646..c851810ffb3 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GenericCommitStatus do
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
index f93904065c7..9d901d01a52 100644
--- a/spec/models/global_milestone_spec.rb
+++ b/spec/models/global_milestone_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GlobalMilestone do
diff --git a/spec/models/gpg_key_spec.rb b/spec/models/gpg_key_spec.rb
index 58a1d2e4ea2..479b39cd139 100644
--- a/spec/models/gpg_key_spec.rb
+++ b/spec/models/gpg_key_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe GpgKey do
diff --git a/spec/models/gpg_key_subkey_spec.rb b/spec/models/gpg_key_subkey_spec.rb
index 3c86837f47f..51d2f9cb9ac 100644
--- a/spec/models/gpg_key_subkey_spec.rb
+++ b/spec/models/gpg_key_subkey_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe GpgKeySubkey do
diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb
index e90319c39b1..47c343edf0e 100644
--- a/spec/models/gpg_signature_spec.rb
+++ b/spec/models/gpg_signature_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe GpgSignature do
diff --git a/spec/models/group_custom_attribute_spec.rb b/spec/models/group_custom_attribute_spec.rb
index 7ecb2022567..7d60c74b62b 100644
--- a/spec/models/group_custom_attribute_spec.rb
+++ b/spec/models/group_custom_attribute_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupCustomAttribute do
diff --git a/spec/models/group_label_spec.rb b/spec/models/group_label_spec.rb
index d0fc1eaa3ec..a3a5c631c3d 100644
--- a/spec/models/group_label_spec.rb
+++ b/spec/models/group_label_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupLabel do
diff --git a/spec/models/group_milestone_spec.rb b/spec/models/group_milestone_spec.rb
index fcc33cd95fe..01856870fe0 100644
--- a/spec/models/group_milestone_spec.rb
+++ b/spec/models/group_milestone_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupMilestone do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 9dc32a815d8..e6e7298a043 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Group do
@@ -362,6 +364,32 @@ describe Group do
it { expect(group.has_maintainer?(nil)).to be_falsey }
end
+ describe '#last_owner?' do
+ before do
+ @members = setup_group_members(group)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_truthy }
+
+ context 'with two owners' do
+ before do
+ create(:group_member, :owner, group: group)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_falsy }
+ end
+
+ context 'with owners from a parent', :postgresql do
+ before do
+ parent_group = create(:group)
+ create(:group_member, :owner, group: parent_group)
+ group.update(parent: parent_group)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_falsy }
+ end
+ end
+
describe '#lfs_enabled?' do
context 'LFS enabled globally' do
before do
@@ -760,14 +788,14 @@ describe Group do
describe '#has_parent?' do
context 'when the group has a parent' do
- it 'should be truthy' do
+ it 'is truthy' do
group = create(:group, :nested)
expect(group.has_parent?).to be_truthy
end
end
context 'when the group has no parent' do
- it 'should be falsy' do
+ it 'is falsy' do
group = create(:group, parent: nil)
expect(group.has_parent?).to be_falsy
end
@@ -810,4 +838,133 @@ describe Group do
it { is_expected.to be_truthy }
end
end
+
+ describe '#first_auto_devops_config' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:group) { create(:group) }
+
+ subject { group.first_auto_devops_config }
+
+ where(:instance_value, :group_value, :config) do
+ # Instance level enabled
+ true | nil | { status: true, scope: :instance }
+ true | true | { status: true, scope: :group }
+ true | false | { status: false, scope: :group }
+
+ # Instance level disabled
+ false | nil | { status: false, scope: :instance }
+ false | true | { status: true, scope: :group }
+ false | false | { status: false, scope: :group }
+ end
+
+ with_them do
+ before do
+ stub_application_setting(auto_devops_enabled: instance_value)
+
+ group.update_attribute(:auto_devops_enabled, group_value)
+ end
+
+ it { is_expected.to eq(config) }
+ end
+
+ context 'with parent groups', :nested_groups do
+ where(:instance_value, :parent_value, :group_value, :config) do
+ # Instance level enabled
+ true | nil | nil | { status: true, scope: :instance }
+ true | nil | true | { status: true, scope: :group }
+ true | nil | false | { status: false, scope: :group }
+
+ true | true | nil | { status: true, scope: :group }
+ true | true | true | { status: true, scope: :group }
+ true | true | false | { status: false, scope: :group }
+
+ true | false | nil | { status: false, scope: :group }
+ true | false | true | { status: true, scope: :group }
+ true | false | false | { status: false, scope: :group }
+
+ # Instance level disable
+ false | nil | nil | { status: false, scope: :instance }
+ false | nil | true | { status: true, scope: :group }
+ false | nil | false | { status: false, scope: :group }
+
+ false | true | nil | { status: true, scope: :group }
+ false | true | true | { status: true, scope: :group }
+ false | true | false | { status: false, scope: :group }
+
+ false | false | nil | { status: false, scope: :group }
+ false | false | true | { status: true, scope: :group }
+ false | false | false | { status: false, scope: :group }
+ end
+
+ with_them do
+ before do
+ stub_application_setting(auto_devops_enabled: instance_value)
+ parent = create(:group, auto_devops_enabled: parent_value)
+
+ group.update!(
+ auto_devops_enabled: group_value,
+ parent: parent
+ )
+ end
+
+ it { is_expected.to eq(config) }
+ end
+ end
+ end
+
+ describe '#auto_devops_enabled?' do
+ subject { group.auto_devops_enabled? }
+
+ context 'when auto devops is explicitly enabled on group' do
+ let(:group) { create(:group, :auto_devops_enabled) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when auto devops is explicitly disabled on group' do
+ let(:group) { create(:group, :auto_devops_disabled) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when auto devops is implicitly enabled or disabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+
+ group.update!(parent: parent_group)
+ end
+
+ context 'when auto devops is enabled on root group' do
+ let(:root_group) { create(:group, :auto_devops_enabled) }
+ let(:subgroup) { create(:group, parent: root_group) }
+ let(:parent_group) { create(:group, parent: subgroup) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when auto devops is disabled on root group' do
+ let(:root_group) { create(:group, :auto_devops_disabled) }
+ let(:subgroup) { create(:group, parent: root_group) }
+ let(:parent_group) { create(:group, parent: subgroup) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when auto devops is disabled on parent group and enabled on root group' do
+ let(:root_group) { create(:group, :auto_devops_enabled) }
+ let(:parent_group) { create(:group, :auto_devops_disabled, parent: root_group) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+ end
+
+ describe 'project_creation_level' do
+ it 'outputs the default one if it is nil' do
+ group = create(:group, project_creation_level: nil)
+
+ expect(group.project_creation_level).to eq(Gitlab::CurrentSettings.default_project_creation)
+ end
+ end
end
diff --git a/spec/models/guest_spec.rb b/spec/models/guest_spec.rb
index fc30f3056e5..93862e98172 100644
--- a/spec/models/guest_spec.rb
+++ b/spec/models/guest_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Guest do
diff --git a/spec/models/hooks/active_hook_filter_spec.rb b/spec/models/hooks/active_hook_filter_spec.rb
index df7edda2213..1249c793f7f 100644
--- a/spec/models/hooks/active_hook_filter_spec.rb
+++ b/spec/models/hooks/active_hook_filter_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ActiveHookFilter do
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 5dd31b1b5de..a945f0d1516 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectHook do
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index e32eaafc13f..936c2fbad27 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ServiceHook do
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index edd1cb455af..e0d4d2e4858 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe SystemHook do
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
index 744a6ccae8b..f812149c9be 100644
--- a/spec/models/hooks/web_hook_log_spec.rb
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe WebHookLog do
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index a308ac6e33a..fe08dc4f5e6 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe WebHook do
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index e1a7a59dfd1..74ddc2d6284 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Identity do
diff --git a/spec/models/import_export_upload_spec.rb b/spec/models/import_export_upload_spec.rb
index 58af84b8a08..18a714f4d98 100644
--- a/spec/models/import_export_upload_spec.rb
+++ b/spec/models/import_export_upload_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ImportExportUpload do
diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb
index cb3d6c7cda2..43954511858 100644
--- a/spec/models/instance_configuration_spec.rb
+++ b/spec/models/instance_configuration_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe InstanceConfiguration do
@@ -30,8 +32,8 @@ describe InstanceConfiguration do
end
def stub_pub_file(exist: true)
- path = 'spec/fixtures/ssh_host_example_key.pub'
- path << 'random' unless exist
+ path = exist ? 'spec/fixtures/ssh_host_example_key.pub' : 'spec/fixtures/ssh_host_example_key.pub.random'
+
allow(subject).to receive(:ssh_algorithm_file).and_return(Rails.root.join(path))
end
end
@@ -80,6 +82,13 @@ describe InstanceConfiguration do
it 'returns the key artifacts_max_size' do
expect(gitlab_ci.keys).to include(:artifacts_max_size)
end
+
+ it 'returns the key artifacts_max_size with values' do
+ stub_application_setting(max_artifacts_size: 200)
+
+ expect(gitlab_ci[:artifacts_max_size][:default]).to eq(100.megabytes)
+ expect(gitlab_ci[:artifacts_max_size][:value]).to eq(200.megabytes)
+ end
end
end
end
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index d32f163f05b..806b4f61bd8 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe InternalId do
@@ -91,7 +93,7 @@ describe InternalId do
before do
described_class.reset_column_information
# Project factory will also call the current_version
- expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ expect(ActiveRecord::Migrator).to receive(:current_version).at_least(:once).and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
end
let(:init) { double('block') }
@@ -102,6 +104,66 @@ describe InternalId do
expect(init).to receive(:call).with(issue).and_return(val)
expect(subject).to eq(val + 1)
end
+
+ it 'always attempts to generate internal IDs in production mode' do
+ allow(Rails.env).to receive(:test?).and_return(false)
+ val = rand(1..100)
+ generator = double(generate: val)
+ expect(InternalId::InternalIdGenerator).to receive(:new).and_return(generator)
+
+ expect(subject).to eq(val)
+ end
+ end
+ end
+
+ describe '.reset' do
+ subject { described_class.reset(issue, scope, usage, value) }
+
+ context 'in the absence of a record' do
+ let(:value) { 2 }
+
+ it 'does not revert back the value' do
+ expect { subject }.not_to change { described_class.count }
+ expect(subject).to be_falsey
+ end
+ end
+
+ context 'when valid iid is used to reset' do
+ let!(:value) { generate_next }
+
+ context 'and iid is a latest one' do
+ it 'does rewind and next generated value is the same' do
+ expect(subject).to be_truthy
+ expect(generate_next).to eq(value)
+ end
+ end
+
+ context 'and iid is not a latest one' do
+ it 'does not rewind' do
+ generate_next
+
+ expect(subject).to be_falsey
+ expect(generate_next).to be > value
+ end
+ end
+
+ def generate_next
+ described_class.generate_next(issue, scope, usage, init)
+ end
+ end
+
+ context 'with an insufficient schema version' do
+ let(:value) { 2 }
+
+ before do
+ described_class.reset_column_information
+ # Project factory will also call the current_version
+ expect(ActiveRecord::Migrator).to receive(:current_version).twice.and_return(InternalId::REQUIRED_SCHEMA_VERSION - 1)
+ end
+
+ it 'does not reset any of the iids' do
+ expect(subject).to be_falsey
+ end
end
end
diff --git a/spec/models/issue/metrics_spec.rb b/spec/models/issue/metrics_spec.rb
index 1bf0ecb98ad..07858fe8a70 100644
--- a/spec/models/issue/metrics_spec.rb
+++ b/spec/models/issue/metrics_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issue::Metrics do
@@ -9,7 +11,7 @@ describe Issue::Metrics do
context "milestones" do
it "records the first time an issue is associated with a milestone" do
time = Time.now
- Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+ Timecop.freeze(time) { subject.update(milestone: create(:milestone, project: project)) }
metrics = subject.metrics
expect(metrics).to be_present
@@ -18,9 +20,9 @@ describe Issue::Metrics do
it "does not record the second time an issue is associated with a milestone" do
time = Time.now
- Timecop.freeze(time) { subject.update(milestone: create(:milestone)) }
+ Timecop.freeze(time) { subject.update(milestone: create(:milestone, project: project)) }
Timecop.freeze(time + 2.hours) { subject.update(milestone: nil) }
- Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone)) }
+ Timecop.freeze(time + 6.hours) { subject.update(milestone: create(:milestone, project: project)) }
metrics = subject.metrics
expect(metrics).to be_present
diff --git a/spec/models/issue_collection_spec.rb b/spec/models/issue_collection_spec.rb
index 580a98193af..7fc635f100f 100644
--- a/spec/models/issue_collection_spec.rb
+++ b/spec/models/issue_collection_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssueCollection do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 6101df2e099..a5c7e9db2a1 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issue do
+ include ExternalAuthorizationServiceHelpers
+
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
it { is_expected.to have_many(:assignees) }
@@ -51,6 +55,29 @@ describe Issue do
end
end
+ describe 'locking' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:lock_version) do
+ [
+ [0],
+ ["0"]
+ ]
+ end
+
+ with_them do
+ it 'works when an issue has a NULL lock_version' do
+ issue = create(:issue)
+
+ described_class.where(id: issue.id).update_all('lock_version = NULL')
+
+ issue.update!(lock_version: lock_version, title: 'locking test')
+
+ expect(issue.reload.title).to eq('locking test')
+ end
+ end
+ end
+
describe '#order_by_position_and_priority' do
let(:project) { create :project }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
@@ -66,6 +93,21 @@ describe Issue do
end
end
+ describe '#sort' do
+ let(:project) { create(:project) }
+
+ context "by relative_position" do
+ let!(:issue) { create(:issue, project: project) }
+ let!(:issue2) { create(:issue, project: project, relative_position: 2) }
+ let!(:issue3) { create(:issue, project: project, relative_position: 1) }
+
+ it "sorts asc with nulls at the end" do
+ issues = project.issues.sort_by_attribute('relative_position')
+ expect(issues).to eq([issue3, issue2, issue])
+ end
+ end
+ end
+
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
@@ -777,4 +819,47 @@ describe Issue do
it_behaves_like 'throttled touch' do
subject { create(:issue, updated_at: 1.hour.ago) }
end
+
+ context 'when an external authentication service' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ describe '#visible_to_user?' do
+ it 'is `false` when an external authorization service is enabled' do
+ issue = build(:issue, project: build(:project, :public))
+
+ expect(issue).not_to be_visible_to_user
+ end
+
+ it 'checks the external service to determine if an issue is readable by a user' do
+ project = build(:project, :public,
+ external_authorization_classification_label: 'a-label')
+ issue = build(:issue, project: project)
+ user = build(:user)
+
+ expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).with(user, 'a-label') { false }
+ expect(issue.visible_to_user?(user)).to be_falsy
+ end
+
+ it 'does not check the external service if a user does not have access to the project' do
+ project = build(:project, :private,
+ external_authorization_classification_label: 'a-label')
+ issue = build(:issue, project: project)
+ user = build(:user)
+
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+ expect(issue.visible_to_user?(user)).to be_falsy
+ end
+
+ it 'does not check the external webservice for admins' do
+ issue = build(:issue)
+ user = build(:admin)
+
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ issue.visible_to_user?(user)
+ end
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 06d26ef89f1..a0b6eff88d5 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Key, :mailer do
diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb
index e2b49bc2de7..b160e72e759 100644
--- a/spec/models/label_link_spec.rb
+++ b/spec/models/label_link_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LabelLink do
diff --git a/spec/models/label_priority_spec.rb b/spec/models/label_priority_spec.rb
index 9dcb0f06b20..1a93468290f 100644
--- a/spec/models/label_priority_spec.rb
+++ b/spec/models/label_priority_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LabelPriority do
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 3fc6c06b7fa..5174c590a10 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Label do
diff --git a/spec/models/legacy_diff_discussion_spec.rb b/spec/models/legacy_diff_discussion_spec.rb
index dae97b69c84..49ea319fbd1 100644
--- a/spec/models/legacy_diff_discussion_spec.rb
+++ b/spec/models/legacy_diff_discussion_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LegacyDiffDiscussion do
diff --git a/spec/models/lfs_download_object_spec.rb b/spec/models/lfs_download_object_spec.rb
index 88838b127d2..effd8b08124 100644
--- a/spec/models/lfs_download_object_spec.rb
+++ b/spec/models/lfs_download_object_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe LfsDownloadObject do
diff --git a/spec/models/lfs_file_lock_spec.rb b/spec/models/lfs_file_lock_spec.rb
index 41ca1578b94..aa64d66944b 100644
--- a/spec/models/lfs_file_lock_spec.rb
+++ b/spec/models/lfs_file_lock_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe LfsFileLock do
diff --git a/spec/models/lfs_object_spec.rb b/spec/models/lfs_object_spec.rb
index 3f929710862..3d4d4b7d795 100644
--- a/spec/models/lfs_object_spec.rb
+++ b/spec/models/lfs_object_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LfsObject do
diff --git a/spec/models/lfs_objects_project_spec.rb b/spec/models/lfs_objects_project_spec.rb
index 0a3180f43e8..3e86ee38566 100644
--- a/spec/models/lfs_objects_project_spec.rb
+++ b/spec/models/lfs_objects_project_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LfsObjectsProject do
diff --git a/spec/models/license_template_spec.rb b/spec/models/license_template_spec.rb
index dd912eefac1..7037277e580 100644
--- a/spec/models/license_template_spec.rb
+++ b/spec/models/license_template_spec.rb
@@ -1,16 +1,19 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe LicenseTemplate do
describe '#content' do
it 'calls a proc exactly once if provided' do
- lazy = build_template(-> { 'bar' })
- content = lazy.content
+ content_proc = -> { 'bar' }
+ expect(content_proc).to receive(:call).once.and_call_original
+
+ lazy = build_template(content_proc)
- expect(content).to eq('bar')
- expect(content.object_id).to eq(lazy.content.object_id)
+ expect(lazy.content).to eq('bar')
- content.replace('foo')
- expect(lazy.content).to eq('foo')
+ # Subsequent calls should not call proc again
+ expect(lazy.content).to eq('bar')
end
it 'returns a string if provided' do
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index a51580f8292..18d4549977c 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe List do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 188beac1582..782a84f922b 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Member do
@@ -68,6 +70,16 @@ describe Member do
expect(child_member).not_to be_valid
end
+ # Membership in a subgroup confers certain access rights, such as being
+ # able to merge or push code to protected branches.
+ it "is valid with an equal level" do
+ child_member.access_level = GroupMember::DEVELOPER
+
+ child_member.validate
+
+ expect(child_member).to be_valid
+ end
+
it "is valid with a higher level" do
child_member.access_level = GroupMember::MAINTAINER
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index a3451c67bd8..f227abd3dae 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -1,6 +1,24 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupMember do
+ describe '.count_users_by_group_id' do
+ it 'counts users by group ID' do
+ user_1 = create(:user)
+ user_2 = create(:user)
+ group_1 = create(:group)
+ group_2 = create(:group)
+
+ group_1.add_owner(user_1)
+ group_1.add_owner(user_2)
+ group_2.add_owner(user_1)
+
+ expect(described_class.count_users_by_group_id).to eq(group_1.id => 2,
+ group_2.id => 1)
+ end
+ end
+
describe '.access_level_roles' do
it 'returns Gitlab::Access.options_with_owner' do
expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner)
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 36bfff2c339..497764b6825 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectMember do
diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb
index 02ff7839739..49573af0fed 100644
--- a/spec/models/merge_request/metrics_spec.rb
+++ b/spec/models/merge_request/metrics_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequest::Metrics do
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 10487190a44..ab2aadf7d88 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe MergeRequestDiffCommit do
diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb
index faa47660a74..66957c24fdc 100644
--- a/spec/models/merge_request_diff_file_spec.rb
+++ b/spec/models/merge_request_diff_file_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe MergeRequestDiffFile do
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 1849d3bac12..a53add67066 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1,8 +1,24 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequestDiff do
+ include RepoHelpers
+
let(:diff_with_commits) { create(:merge_request).merge_request_diff }
+ describe 'validations' do
+ subject { diff_with_commits }
+
+ it 'checks sha format of base_commit_sha, head_commit_sha and start_commit_sha' do
+ subject.base_commit_sha = subject.head_commit_sha = subject.start_commit_sha = 'foobar'
+
+ expect(subject.valid?).to be false
+ expect(subject.errors.count).to eq 3
+ expect(subject.errors).to all(include('is not a valid SHA'))
+ end
+ end
+
describe 'create new record' do
subject { diff_with_commits }
@@ -37,7 +53,104 @@ describe MergeRequestDiff do
end
end
- describe '#latest' do
+ describe '.ids_for_external_storage_migration' do
+ set(:merge_request) { create(:merge_request) }
+ set(:outdated) { merge_request.merge_request_diff }
+ set(:latest) { merge_request.create_merge_request_diff }
+
+ set(:closed_mr) { create(:merge_request, :closed_last_month) }
+ let(:closed) { closed_mr.merge_request_diff }
+
+ set(:merged_mr) { create(:merge_request, :merged_last_month) }
+ let(:merged) { merged_mr.merge_request_diff }
+
+ set(:recently_closed_mr) { create(:merge_request, :closed) }
+ let(:closed_recently) { recently_closed_mr.merge_request_diff }
+
+ set(:recently_merged_mr) { create(:merge_request, :merged) }
+ let(:merged_recently) { recently_merged_mr.merge_request_diff }
+
+ before do
+ merge_request.update!(latest_merge_request_diff: latest)
+ end
+
+ subject { described_class.ids_for_external_storage_migration(limit: 1000) }
+
+ context 'external diffs are disabled' do
+ before do
+ stub_external_diffs_setting(enabled: false)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'external diffs are misconfigured' do
+ before do
+ stub_external_diffs_setting(enabled: true, when: 'every second tuesday')
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'external diffs are enabled unconditionally' do
+ before do
+ stub_external_diffs_setting(enabled: true)
+ end
+
+ it { is_expected.to contain_exactly(outdated.id, latest.id, closed.id, merged.id, closed_recently.id, merged_recently.id) }
+ end
+
+ context 'external diffs are enabled for outdated diffs' do
+ before do
+ stub_external_diffs_setting(enabled: true, when: 'outdated')
+ end
+
+ it 'returns records for outdated merge request versions' do
+ is_expected.to contain_exactly(outdated.id, closed.id, merged.id)
+ end
+ end
+
+ context 'with limit' do
+ it 'respects the limit' do
+ stub_external_diffs_setting(enabled: true)
+
+ expect(described_class.ids_for_external_storage_migration(limit: 3).count).to eq(3)
+ end
+ end
+ end
+
+ describe '#migrate_files_to_external_storage!' do
+ let(:diff) { create(:merge_request).merge_request_diff }
+
+ it 'converts from in-database to external storage' do
+ expect(diff).not_to be_stored_externally
+
+ stub_external_diffs_setting(enabled: true)
+ expect(diff).to receive(:save!)
+
+ diff.migrate_files_to_external_storage!
+
+ expect(diff).to be_stored_externally
+ end
+
+ it 'does nothing with an external diff' do
+ stub_external_diffs_setting(enabled: true)
+
+ expect(diff).to be_stored_externally
+ expect(diff).not_to receive(:save!)
+
+ diff.migrate_files_to_external_storage!
+ end
+
+ it 'does nothing if external diffs are disabled' do
+ expect(diff).not_to be_stored_externally
+ expect(diff).not_to receive(:save!)
+
+ diff.migrate_files_to_external_storage!
+ end
+ end
+
+ describe '#latest?' do
let!(:mr) { create(:merge_request, :with_diffs) }
let!(:first_diff) { mr.merge_request_diff }
let!(:last_diff) { mr.create_merge_request_diff }
@@ -78,7 +191,7 @@ describe MergeRequestDiff do
it 'returns persisted diffs if cannot compare with diff refs' do
expect(diff).to receive(:load_diffs).and_call_original
- diff.update!(head_commit_sha: 'invalid-sha')
+ diff.update!(head_commit_sha: Digest::SHA1.hexdigest(SecureRandom.hex))
diff.diffs.diff_files
end
@@ -182,6 +295,25 @@ describe MergeRequestDiff do
expect(diff_file).to be_binary
expect(diff_file.diff).to eq(mr_diff.compare.diffs(paths: [path]).to_a.first.diff)
end
+
+ context 'with diffs that contain a null byte' do
+ let(:filename) { 'test-null.txt' }
+ let(:content) { "a" * 10000 + "\x00" }
+ let(:project) { create(:project, :repository) }
+ let(:branch) { 'null-data' }
+ let(:target_branch) { 'master' }
+
+ it 'saves diffs correctly' do
+ create_file_in_repo(project, target_branch, branch, filename, content)
+
+ mr_diff = create(:merge_request, target_project: project, source_project: project, source_branch: branch, target_branch: target_branch).merge_request_diff
+ diff_file = mr_diff.merge_request_diff_files.find_by(new_path: filename)
+
+ expect(diff_file).to be_binary
+ expect(diff_file.diff).to eq(mr_diff.compare.diffs(paths: [filename]).to_a.first.diff)
+ expect(diff_file.diff).to include(content)
+ end
+ end
end
end
@@ -189,12 +321,56 @@ describe MergeRequestDiff do
include_examples 'merge request diffs'
end
- describe 'external diffs configured' do
+ describe 'external diffs always enabled' do
before do
- stub_external_diffs_setting(enabled: true)
+ stub_external_diffs_setting(enabled: true, when: 'always')
+ end
+
+ include_examples 'merge request diffs'
+ end
+
+ describe 'exernal diffs enabled for outdated diffs' do
+ before do
+ stub_external_diffs_setting(enabled: true, when: 'outdated')
end
include_examples 'merge request diffs'
+
+ it 'stores up-to-date diffs in the database' do
+ expect(diff).not_to be_stored_externally
+ end
+
+ it 'stores diffs for recently closed MRs in the database' do
+ mr = create(:merge_request, :closed)
+
+ expect(mr.merge_request_diff).not_to be_stored_externally
+ end
+
+ it 'stores diffs for recently merged MRs in the database' do
+ mr = create(:merge_request, :merged)
+
+ expect(mr.merge_request_diff).not_to be_stored_externally
+ end
+
+ it 'stores diffs for old MR versions in external storage' do
+ old_diff = diff
+ merge_request.create_merge_request_diff
+ old_diff.migrate_files_to_external_storage!
+
+ expect(old_diff).to be_stored_externally
+ end
+
+ it 'stores diffs for old closed MRs in external storage' do
+ mr = create(:merge_request, :closed_last_month)
+
+ expect(mr.merge_request_diff).to be_stored_externally
+ end
+
+ it 'stores diffs for old merged MRs in external storage' do
+ mr = create(:merge_request, :merged_last_month)
+
+ expect(mr.merge_request_diff).to be_stored_externally
+ end
end
describe '#commit_shas' do
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 82a853a23b9..fc28c216b21 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequest do
@@ -11,7 +13,7 @@ describe MergeRequest do
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
- it { is_expected.to belong_to(:assignee) }
+ it { is_expected.to have_many(:assignees).through(:merge_request_assignees) }
it { is_expected.to have_many(:merge_request_diffs) }
context 'for forks' do
@@ -29,6 +31,29 @@ describe MergeRequest do
end
end
+ describe 'locking' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:lock_version) do
+ [
+ [0],
+ ["0"]
+ ]
+ end
+
+ with_them do
+ it 'works when a merge request has a NULL lock_version' do
+ merge_request = create(:merge_request)
+
+ described_class.where(id: merge_request.id).update_all('lock_version = NULL')
+
+ merge_request.update!(lock_version: lock_version, title: 'locking test')
+
+ expect(merge_request.reload.title).to eq('locking test')
+ end
+ end
+ end
+
describe '#squash_in_progress?' do
let(:repo_path) do
Gitlab::GitalyClient::StorageSettings.allow_disk_access do
@@ -84,32 +109,27 @@ describe MergeRequest do
describe '#default_squash_commit_message' do
let(:project) { subject.project }
-
- def commit_collection(commit_hashes)
- raw_commits = commit_hashes.map { |raw| Commit.from_hash(raw, project) }
-
- CommitCollection.new(project, raw_commits)
- end
+ let(:is_multiline) { -> (c) { c.description.present? } }
+ let(:multiline_commits) { subject.commits.select(&is_multiline) }
+ let(:singleline_commits) { subject.commits.reject(&is_multiline) }
it 'returns the oldest multiline commit message' do
- commits = commit_collection([
- { message: 'Singleline', parent_ids: [] },
- { message: "Second multiline\nCommit message", parent_ids: [] },
- { message: "First multiline\nCommit message", parent_ids: [] }
- ])
-
- expect(subject).to receive(:commits).and_return(commits)
-
- expect(subject.default_squash_commit_message).to eq("First multiline\nCommit message")
+ expect(subject.default_squash_commit_message).to eq(multiline_commits.last.message)
end
it 'returns the merge request title if there are no multiline commits' do
- commits = commit_collection([
- { message: 'Singleline', parent_ids: [] }
- ])
+ expect(subject).to receive(:commits).and_return(
+ CommitCollection.new(project, singleline_commits)
+ )
- expect(subject).to receive(:commits).and_return(commits)
+ expect(subject.default_squash_commit_message).to eq(subject.title)
+ end
+ it 'does not return commit messages from multiline merge commits' do
+ collection = CommitCollection.new(project, multiline_commits).enrich!
+
+ expect(collection.commits).to all( receive(:merge_commit?).and_return(true) )
+ expect(subject).to receive(:commits).and_return(collection)
expect(subject.default_squash_commit_message).to eq(subject.title)
end
end
@@ -153,6 +173,42 @@ describe MergeRequest do
end
end
+ context 'for branch' do
+ before do
+ stub_feature_flags(stricter_mr_branch_name: false)
+ end
+
+ using RSpec::Parameterized::TableSyntax
+
+ where(:branch_name, :valid) do
+ 'foo' | true
+ 'foo:bar' | false
+ '+foo:bar' | false
+ 'foo bar' | false
+ '-foo' | false
+ 'HEAD' | true
+ 'refs/heads/master' | true
+ end
+
+ with_them do
+ it "validates source_branch" do
+ subject = build(:merge_request, source_branch: branch_name, target_branch: 'master')
+
+ subject.valid?
+
+ expect(subject.errors.added?(:source_branch)).to eq(!valid)
+ end
+
+ it "validates target_branch" do
+ subject = build(:merge_request, source_branch: 'master', target_branch: branch_name)
+
+ subject.valid?
+
+ expect(subject.errors.added?(:target_branch)).to eq(!valid)
+ end
+ end
+ end
+
context 'for forks' do
let(:project) { create(:project) }
let(:fork1) { fork_project(project) }
@@ -270,6 +326,25 @@ describe MergeRequest do
end
end
+ describe '.recent_target_branches' do
+ let(:project) { create(:project) }
+ let!(:merge_request1) { create(:merge_request, :opened, source_project: project, target_branch: 'feature') }
+ let!(:merge_request2) { create(:merge_request, :closed, source_project: project, target_branch: 'merge-test') }
+ let!(:merge_request3) { create(:merge_request, :opened, source_project: project, target_branch: 'fix') }
+ let!(:merge_request4) { create(:merge_request, :closed, source_project: project, target_branch: 'feature') }
+
+ before do
+ merge_request1.update_columns(updated_at: 1.day.since)
+ merge_request2.update_columns(updated_at: 2.days.since)
+ merge_request3.update_columns(updated_at: 3.days.since)
+ merge_request4.update_columns(updated_at: 4.days.since)
+ end
+
+ it 'returns target branches sort by updated at desc' do
+ expect(described_class.recent_target_branches).to match_array(['feature', 'merge-test', 'fix'])
+ end
+ end
+
describe '#target_branch_sha' do
let(:project) { create(:project, :repository) }
@@ -296,34 +371,18 @@ describe MergeRequest do
describe '#card_attributes' do
it 'includes the author name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
- allow(subject).to receive(:assignee).and_return(nil)
+ allow(subject).to receive(:assignees).and_return([])
expect(subject.card_attributes)
- .to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+ .to eq({ 'Author' => 'Robert', 'Assignee' => "" })
end
- it 'includes the assignee name' do
+ it 'includes the assignees name' do
allow(subject).to receive(:author).and_return(double(name: 'Robert'))
- allow(subject).to receive(:assignee).and_return(double(name: 'Douwe'))
+ allow(subject).to receive(:assignees).and_return([double(name: 'Douwe'), double(name: 'Robert')])
expect(subject.card_attributes)
- .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
- end
- end
-
- describe '#assignee_ids' do
- it 'returns an array of the assigned user id' do
- subject.assignee_id = 123
-
- expect(subject.assignee_ids).to eq([123])
- end
- end
-
- describe '#assignee_ids=' do
- it 'sets assignee_id to the last id in the array' do
- subject.assignee_ids = [123, 456]
-
- expect(subject.assignee_id).to eq(456)
+ .to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe and Robert' })
end
end
@@ -331,7 +390,7 @@ describe MergeRequest do
let(:user) { create(:user) }
it 'returns true for a user that is assigned to a merge request' do
- subject.assignee = user
+ subject.assignees = [user]
expect(subject.assignee_or_author?(user)).to eq(true)
end
@@ -435,7 +494,6 @@ describe MergeRequest do
it 'does not cache issues from external trackers' do
issue = ExternalIssue.new('JIRA-123', subject.project)
commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
-
allow(subject).to receive(:commits).and_return([commit])
expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to raise_error
@@ -765,6 +823,14 @@ describe MergeRequest do
expect(merge_request.commits).not_to be_empty
expect(merge_request.related_notes.count).to eq(3)
end
+
+ it "excludes system notes for commits" do
+ system_note = create(:note_on_commit, :system, commit_id: merge_request.commits.first.id,
+ project: merge_request.project)
+
+ expect(merge_request.related_notes.count).to eq(2)
+ expect(merge_request.related_notes).not_to include(system_note)
+ end
end
describe '#for_fork?' do
@@ -1008,47 +1074,31 @@ describe MergeRequest do
end
end
- describe "#reset_merge_when_pipeline_succeeds" do
- let(:merge_if_green) do
- create :merge_request, merge_when_pipeline_succeeds: true, merge_user: create(:user),
- merge_params: { "should_remove_source_branch" => "1", "commit_message" => "msg" }
- end
+ describe "#auto_merge_strategy" do
+ subject { merge_request.auto_merge_strategy }
- it "sets the item to false" do
- merge_if_green.reset_merge_when_pipeline_succeeds
- merge_if_green.reload
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
- expect(merge_if_green.merge_when_pipeline_succeeds).to be_falsey
- expect(merge_if_green.merge_params["should_remove_source_branch"]).to be_nil
- expect(merge_if_green.merge_params["commit_message"]).to be_nil
- end
- end
+ it { is_expected.to eq('merge_when_pipeline_succeeds') }
- describe '#commit_authors' do
- it 'returns all the authors of every commit in the merge request' do
- users = subject.commits.map(&:author_email).uniq.map do |email|
- create(:user, email: email)
- end
+ context 'when auto merge is disabled' do
+ let(:merge_request) { create(:merge_request) }
- expect(subject.commit_authors).to match_array(users)
- end
-
- it 'returns an empty array if no author is associated with a user' do
- expect(subject.commit_authors).to be_empty
+ it { is_expected.to be_nil }
end
end
- describe '#authors' do
- it 'returns a list with all the commit authors in the merge request and author' do
- users = subject.commits.map(&:author_email).uniq.map do |email|
+ describe '#committers' do
+ it 'returns all the committers of every commit in the merge request' do
+ users = subject.commits.without_merge_commits.map(&:committer_email).uniq.map do |email|
create(:user, email: email)
end
- expect(subject.authors).to match_array([subject.author, *users])
+ expect(subject.committers).to match_array(users)
end
- it 'returns only the author if no committer is associated with a user' do
- expect(subject.authors).to contain_exactly(subject.author)
+ it 'returns an empty array if no committer is associated with a user' do
+ expect(subject.committers).to be_empty
end
end
@@ -1168,8 +1218,10 @@ describe MergeRequest do
end
context 'head pipeline' do
+ let(:diff_head_sha) { Digest::SHA1.hexdigest(SecureRandom.hex) }
+
before do
- allow(subject).to receive(:diff_head_sha).and_return('lastsha')
+ allow(subject).to receive(:diff_head_sha).and_return(diff_head_sha)
end
describe '#head_pipeline' do
@@ -1197,7 +1249,15 @@ describe MergeRequest do
end
it 'returns the pipeline for MR with recent pipeline' do
- pipeline = create(:ci_empty_pipeline, sha: 'lastsha')
+ pipeline = create(:ci_empty_pipeline, sha: diff_head_sha)
+ subject.update_attribute(:head_pipeline_id, pipeline.id)
+
+ expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
+ expect(subject.actual_head_pipeline).to eq(pipeline)
+ end
+
+ it 'returns the pipeline for MR with recent merge request pipeline' do
+ pipeline = create(:ci_empty_pipeline, sha: 'merge-sha', source_sha: diff_head_sha)
subject.update_attribute(:head_pipeline_id, pipeline.id)
expect(subject.actual_head_pipeline).to eq(subject.head_pipeline)
@@ -1331,9 +1391,9 @@ describe MergeRequest do
sha: shas.second)
end
- let!(:merge_request_pipeline) do
+ let!(:detached_merge_request_pipeline) do
create(:ci_pipeline,
- source: :merge_request,
+ source: :merge_request_event,
project: project,
ref: source_ref,
sha: shas.second,
@@ -1357,7 +1417,7 @@ describe MergeRequest do
it 'returns merge request pipeline first' do
expect(merge_request.all_pipelines)
- .to eq([merge_request_pipeline,
+ .to eq([detached_merge_request_pipeline,
branch_pipeline])
end
@@ -1370,9 +1430,9 @@ describe MergeRequest do
sha: shas.first)
end
- let!(:merge_request_pipeline_2) do
+ let!(:detached_merge_request_pipeline_2) do
create(:ci_pipeline,
- source: :merge_request,
+ source: :merge_request_event,
project: project,
ref: source_ref,
sha: shas.first,
@@ -1381,8 +1441,8 @@ describe MergeRequest do
it 'returns merge request pipelines first' do
expect(merge_request.all_pipelines)
- .to eq([merge_request_pipeline_2,
- merge_request_pipeline,
+ .to eq([detached_merge_request_pipeline_2,
+ detached_merge_request_pipeline,
branch_pipeline_2,
branch_pipeline])
end
@@ -1397,9 +1457,9 @@ describe MergeRequest do
sha: shas.first)
end
- let!(:merge_request_pipeline_2) do
+ let!(:detached_merge_request_pipeline_2) do
create(:ci_pipeline,
- source: :merge_request,
+ source: :merge_request_event,
project: project,
ref: source_ref,
sha: shas.first,
@@ -1420,16 +1480,35 @@ describe MergeRequest do
it 'returns only related merge request pipelines' do
expect(merge_request.all_pipelines)
- .to eq([merge_request_pipeline,
+ .to eq([detached_merge_request_pipeline,
branch_pipeline_2,
branch_pipeline])
expect(merge_request_2.all_pipelines)
- .to eq([merge_request_pipeline_2,
+ .to eq([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
@@ -1470,6 +1549,37 @@ describe MergeRequest do
end
end
+ context 'when detached merge request pipeline is run on head ref of the merge request' do
+ let!(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ project: merge_request.source_project,
+ ref: merge_request.ref_path,
+ sha: sha,
+ merge_request: merge_request)
+ end
+
+ let(:sha) { merge_request.diff_head_sha }
+
+ it 'sets the head ref of the merge request to the pipeline ref' do
+ expect(pipeline.ref).to match(%r{refs/merge-requests/\d+/head})
+ end
+
+ it 'updates correctly even though the target branch name of the merge request is different from the pipeline ref' do
+ expect { subject }
+ .to change { merge_request.reload.head_pipeline }
+ .from(nil).to(pipeline)
+ end
+
+ context 'when sha is not HEAD of the source branch' do
+ let(:sha) { merge_request.diff_base_sha }
+
+ it 'does not update head pipeline' do
+ expect { subject }.not_to change { merge_request.reload.head_pipeline }
+ end
+ end
+ end
+
context 'when there are no pipelines with the diff head sha' do
it 'does not update the head pipeline' do
expect { subject }
@@ -1855,15 +1965,14 @@ describe MergeRequest do
it 'updates when assignees change' do
user1 = create(:user)
user2 = create(:user)
- mr = create(:merge_request, assignee: user1)
+ mr = create(:merge_request, assignees: [user1])
mr.project.add_developer(user1)
mr.project.add_developer(user2)
expect(user1.assigned_open_merge_requests_count).to eq(1)
expect(user2.assigned_open_merge_requests_count).to eq(0)
- mr.assignee = user2
- mr.save
+ mr.assignees = [user2]
expect(user1.assigned_open_merge_requests_count).to eq(0)
expect(user2.assigned_open_merge_requests_count).to eq(1)
@@ -1887,57 +1996,6 @@ describe MergeRequest do
end
end
- describe '#check_if_can_be_merged' do
- let(:project) { create(:project, only_allow_merge_if_pipeline_succeeds: true) }
-
- shared_examples 'checking if can be merged' do
- context 'when it is not broken and has no conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(true)
- end
-
- it 'is marked as mergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('can_be_merged')
- end
- end
-
- context 'when broken' do
- before do
- allow(subject).to receive(:broken?) { true }
- allow(project.repository).to receive(:can_be_merged?).and_return(false)
- end
-
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
- end
- end
-
- context 'when it has conflicts' do
- before do
- allow(subject).to receive(:broken?) { false }
- allow(project.repository).to receive(:can_be_merged?).and_return(false)
- end
-
- it 'becomes unmergeable' do
- expect { subject.check_if_can_be_merged }.to change { subject.merge_status }.to('cannot_be_merged')
- end
- end
- end
-
- context 'when merge_status is unchecked' do
- subject { create(:merge_request, source_project: project, merge_status: :unchecked) }
-
- it_behaves_like 'checking if can be merged'
- end
-
- context 'when merge_status is unchecked' do
- subject { create(:merge_request, source_project: project, merge_status: :cannot_be_merged_recheck) }
-
- it_behaves_like 'checking if can be merged'
- end
- end
-
describe '#mergeable?' do
let(:project) { create(:project) }
@@ -1951,7 +2009,7 @@ describe MergeRequest do
it 'return true if #mergeable_state? is true and the MR #can_be_merged? is true' do
allow(subject).to receive(:mergeable_state?) { true }
- expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:check_mergeability)
expect(subject).to receive(:can_be_merged?) { true }
expect(subject.mergeable?).to be_truthy
@@ -1965,7 +2023,7 @@ describe MergeRequest do
it 'checks if merge request can be merged' do
allow(subject).to receive(:mergeable_ci_state?) { true }
- expect(subject).to receive(:check_if_can_be_merged)
+ expect(subject).to receive(:check_mergeability)
subject.mergeable?
end
@@ -2071,7 +2129,7 @@ describe MergeRequest do
end
context 'when merges are not restricted to green builds' do
- subject { build(:merge_request, target_project: build(:project, only_allow_merge_if_pipeline_succeeds: false)) }
+ subject { build(:merge_request, target_project: create(:project, only_allow_merge_if_pipeline_succeeds: false)) }
context 'and a failed pipeline is associated' do
before do
@@ -2210,6 +2268,50 @@ describe MergeRequest do
end
end
+ describe "#environments" do
+ subject { merge_request.environments }
+
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+ let(:project) { merge_request.project }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request, project: project,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let!(:job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
+
+ it 'returns environments' do
+ is_expected.to eq(pipeline.environments)
+ expect(subject.count).to be(1)
+ end
+
+ context 'when pipeline is not associated with environments' do
+ let!(:job) { create(:ci_build, pipeline: pipeline, project: project) }
+
+ it 'returns empty array' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when pipeline is not a pipeline for merge request' do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ it 'returns empty relation' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
describe "#reload_diff" do
it 'calls MergeRequests::ReloadDiffsService#execute with correct params' do
user = create(:user)
@@ -2613,14 +2715,21 @@ describe MergeRequest do
end
describe '#has_commits?' do
- before do
+ it 'returns true when merge request diff has commits' do
allow(subject.merge_request_diff).to receive(:commits_count)
.and_return(2)
- end
- it 'returns true when merge request diff has commits' do
expect(subject.has_commits?).to be_truthy
end
+
+ context 'when commits_count is nil' do
+ it 'returns false' do
+ allow(subject.merge_request_diff).to receive(:commits_count)
+ .and_return(nil)
+
+ expect(subject.has_commits?).to be_falsey
+ end
+ end
end
describe '#has_no_commits?' do
@@ -3016,4 +3125,32 @@ describe MergeRequest do
end
end
end
+
+ describe '.merge_request_ref?' do
+ subject { described_class.merge_request_ref?(ref) }
+
+ context 'when ref is ref name of a branch' do
+ let(:ref) { 'feature' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when ref is HEAD ref path of a branch' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when ref is HEAD ref path of a merge request' do
+ let(:ref) { 'refs/merge-requests/1/head' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when ref is merge ref path of a merge request' do
+ let(:ref) { 'refs/merge-requests/1/merge' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index af7e3d3a6c9..3704a2d468d 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Milestone do
@@ -29,12 +31,28 @@ describe Milestone do
end
describe 'start_date' do
- it 'adds an error when start_date is greated then due_date' do
+ it 'adds an error when start_date is greater then due_date' do
milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday)
expect(milestone).not_to be_valid
expect(milestone.errors[:due_date]).to include("must be greater than start date")
end
+
+ it 'adds an error when start_date is greater than 9999-12-31' do
+ milestone = build(:milestone, start_date: Date.new(10000, 1, 1))
+
+ expect(milestone).not_to be_valid
+ expect(milestone.errors[:start_date]).to include("date must not be after 9999-12-31")
+ end
+ end
+
+ describe 'due_date' do
+ it 'adds an error when due_date is greater than 9999-12-31' do
+ milestone = build(:milestone, due_date: Date.new(10000, 1, 1))
+
+ expect(milestone).not_to be_valid
+ expect(milestone.errors[:due_date]).to include("date must not be after 9999-12-31")
+ end
end
end
@@ -164,38 +182,16 @@ describe Milestone do
end
end
- describe '#percent_complete' do
- before do
- allow(milestone).to receive_messages(
- closed_items_count: 3,
- total_items_count: 4
- )
- end
-
- it { expect(milestone.percent_complete(user)).to eq(75) }
- end
-
describe '#can_be_closed?' do
it { expect(milestone.can_be_closed?).to be_truthy }
end
- describe '#total_items_count' do
- before do
- create :closed_issue, milestone: milestone, project: project
- create :merge_request, milestone: milestone
- end
-
- it 'returns total count of issues and merge requests assigned to milestone' do
- expect(milestone.total_items_count(user)).to eq 2
- end
- end
-
describe '#can_be_closed?' do
before do
- milestone = create :milestone
- create :closed_issue, milestone: milestone
+ milestone = create :milestone, project: project
+ create :closed_issue, milestone: milestone, project: project
- create :issue
+ create :issue, project: project
end
it 'returns true if milestone active and all nested issues closed' do
@@ -502,4 +498,20 @@ describe Milestone do
end
end
end
+
+ describe '.reference_pattern' do
+ subject { described_class.reference_pattern }
+
+ it { is_expected.to match('gitlab-org/gitlab-ce%123') }
+ it { is_expected.to match('gitlab-org/gitlab-ce%"my-milestone"') }
+ end
+
+ describe '.link_reference_pattern' do
+ subject { described_class.link_reference_pattern }
+
+ it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/milestones/123") }
+ it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/-/milestones/123") }
+ it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-ce/issues/123") }
+ it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
+ end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 475fbe56e4d..d80183af33e 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Namespace do
@@ -61,6 +63,11 @@ describe Namespace do
end
end
+ describe 'delegate' do
+ it { is_expected.to delegate_method(:name).to(:owner).with_prefix.with_arguments(allow_nil: true) }
+ it { is_expected.to delegate_method(:avatar_url).to(:owner).with_arguments(allow_nil: true) }
+ end
+
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
it { is_expected.to respond_to(:to_param) }
@@ -139,20 +146,22 @@ describe Namespace do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- storage_size: 606,
repository_size: 101,
+ wiki_size: 505,
lfs_objects_size: 202,
- build_artifacts_size: 303))
+ build_artifacts_size: 303,
+ packages_size: 404))
end
let(:project2) do
create(:project,
namespace: namespace,
statistics: build(:project_statistics,
- storage_size: 60,
repository_size: 10,
+ wiki_size: 50,
lfs_objects_size: 20,
- build_artifacts_size: 30))
+ build_artifacts_size: 30,
+ packages_size: 40))
end
it "sums all project storage counters in the namespace" do
@@ -160,10 +169,12 @@ describe Namespace do
project2
statistics = described_class.with_statistics.find(namespace.id)
- expect(statistics.storage_size).to eq 666
+ expect(statistics.storage_size).to eq 1665
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.packages_size).to eq 444
end
it "correctly handles namespaces without projects" do
@@ -171,8 +182,10 @@ describe Namespace do
expect(statistics.storage_size).to eq 0
expect(statistics.repository_size).to eq 0
+ 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.packages_size).to eq 0
end
end
@@ -736,43 +749,90 @@ describe Namespace do
end
end
- describe '#full_path_was' do
+ describe '#full_path_before_last_save' do
context 'when the group has no parent' do
- it 'should return the path was' do
- group = create(:group, parent: nil)
- expect(group.full_path_was).to eq(group.path_was)
+ it 'returns the path before last save' do
+ group = create(:group)
+
+ group.update(parent: nil)
+
+ expect(group.full_path_before_last_save).to eq(group.path_before_last_save)
end
end
context 'when a parent is assigned to a group with no previous parent' do
- it 'should return the path was' do
+ it 'returns the path before last save' do
group = create(:group, parent: nil)
-
parent = create(:group)
- group.parent = parent
- expect(group.full_path_was).to eq("#{group.path_was}")
+ group.update(parent: parent)
+
+ expect(group.full_path_before_last_save).to eq("#{group.path_before_last_save}")
end
end
context 'when a parent is removed from the group' do
- it 'should return the parent full path' do
+ it 'returns the parent full path' do
parent = create(:group)
group = create(:group, parent: parent)
- group.parent = nil
- expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}")
+ group.update(parent: nil)
+
+ expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}")
end
end
context 'when changing parents' do
- it 'should return the previous parent full path' do
+ it 'returns the previous parent full path' do
parent = create(:group)
group = create(:group, parent: parent)
new_parent = create(:group)
- group.parent = new_parent
- expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}")
+
+ group.update(parent: new_parent)
+
+ expect(group.full_path_before_last_save).to eq("#{parent.full_path}/#{group.path}")
end
end
end
+
+ describe '#auto_devops_enabled' do
+ context 'with users' do
+ let(:user) { create(:user) }
+
+ subject { user.namespace.auto_devops_enabled? }
+
+ before do
+ user.namespace.update!(auto_devops_enabled: auto_devops_enabled)
+ end
+
+ context 'when auto devops is explicitly enabled' do
+ let(:auto_devops_enabled) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when auto devops is explicitly disabled' do
+ let(:auto_devops_enabled) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#user?' do
+ subject { namespace.user? }
+
+ context 'when type is a user' do
+ let(:user) { create(:user) }
+ let(:namespace) { user.namespace }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when type is a group' do
+ let(:namespace) { create(:group) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
index c364dd6643b..232172fde76 100644
--- a/spec/models/network/graph_spec.rb
+++ b/spec/models/network/graph_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Network::Graph do
@@ -20,7 +22,7 @@ describe Network::Graph do
expect(commits).to all( be_kind_of(Network::Commit) )
end
- it 'it the commits by commit date (descending)' do
+ it 'sorts commits by commit date (descending)' do
# Remove duplicate timestamps because they make it harder to
# assert that the commits are sorted as expected.
commits = graph.commits.uniq(&:date)
diff --git a/spec/models/note_diff_file_spec.rb b/spec/models/note_diff_file_spec.rb
index 591c1a89748..b15bedd257e 100644
--- a/spec/models/note_diff_file_spec.rb
+++ b/spec/models/note_diff_file_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe NoteDiffFile do
@@ -8,4 +10,31 @@ describe NoteDiffFile do
describe 'validations' do
it { is_expected.to validate_presence_of(:diff_note) }
end
+
+ describe '.referencing_sha' do
+ let!(:diff_note) { create(:diff_note_on_commit) }
+
+ let(:note_diff_file) { diff_note.note_diff_file }
+ let(:project) { diff_note.project }
+
+ it 'finds note diff files by project and sha' do
+ found = described_class.referencing_sha(diff_note.commit_id, project_id: project.id)
+
+ expect(found).to contain_exactly(note_diff_file)
+ end
+
+ it 'excludes note diff files with the wrong project' do
+ other_project = create(:project)
+
+ found = described_class.referencing_sha(diff_note.commit_id, project_id: other_project.id)
+
+ expect(found).to be_empty
+ end
+
+ it 'excludes note diff files with the wrong sha' do
+ found = described_class.referencing_sha(Gitlab::Git::BLANK_SHA, project_id: project.id)
+
+ expect(found).to be_empty
+ end
+ end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 385b8a7959f..7a1ab20186a 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Note do
@@ -208,6 +210,24 @@ describe Note do
end
end
+ describe "edited?" do
+ let(:note) { build(:note, updated_by_id: nil, created_at: Time.now, updated_at: Time.now + 5.hours) }
+
+ context "with updated_by" do
+ it "returns true" do
+ note.updated_by = build(:user)
+
+ expect(note.edited?).to be_truthy
+ end
+ end
+
+ context "without updated_by" do
+ it "returns false" do
+ expect(note.edited?).to be_falsy
+ end
+ end
+ end
+
describe "confidential?" do
it "delegates to noteable" do
issue_note = build(:note, :on_issue)
diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb
index 13fe47799ed..1b1ede6b14c 100644
--- a/spec/models/notification_recipient_spec.rb
+++ b/spec/models/notification_recipient_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NotificationRecipient do
@@ -7,11 +9,43 @@ describe NotificationRecipient do
subject(:recipient) { described_class.new(user, :watch, target: target, project: project) }
- it 'denies access to a target when cross project access is denied' do
- allow(Ability).to receive(:allowed?).and_call_original
- expect(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(false)
+ describe '#has_access?' do
+ before do
+ allow(user).to receive(:can?).and_call_original
+ end
+
+ context 'user cannot read cross project' do
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_cross_project).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
+
+ context 'user cannot read build' do
+ let(:target) { build(:ci_pipeline) }
+
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_build, target).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
+
+ context 'user cannot read commit' do
+ let(:target) { build(:commit) }
+
+ it 'returns false' do
+ expect(user).to receive(:can?).with(:read_commit, target).and_return(false)
+ expect(recipient.has_access?).to eq false
+ end
+ end
+
+ context 'target has no policy' do
+ let(:target) { double.as_null_object }
- expect(recipient.has_access?).to be_falsy
+ it 'returns true' do
+ expect(recipient.has_access?).to eq true
+ end
+ end
end
context '#notification_setting' do
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index c8ab564e3bc..85128456918 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe NotificationSetting do
diff --git a/spec/models/pages_domain_acme_order_spec.rb b/spec/models/pages_domain_acme_order_spec.rb
new file mode 100644
index 00000000000..4ffb4fc7389
--- /dev/null
+++ b/spec/models/pages_domain_acme_order_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomainAcmeOrder do
+ using RSpec::Parameterized::TableSyntax
+
+ describe '.expired' do
+ let!(:not_expired_order) { create(:pages_domain_acme_order) }
+ let!(:expired_order) { create(:pages_domain_acme_order, :expired) }
+
+ it 'returns only expired orders' do
+ expect(described_class.count).to eq(2)
+ expect(described_class.expired).to eq([expired_order])
+ end
+ end
+
+ describe '.find_by_domain_and_token' do
+ let!(:domain) { create(:pages_domain, domain: 'test.com') }
+ let!(:acme_order) { create(:pages_domain_acme_order, challenge_token: 'righttoken', pages_domain: domain) }
+
+ where(:domain_name, :challenge_token, :present) do
+ 'test.com' | 'righttoken' | true
+ 'test.com' | 'wrongtoken' | false
+ 'test.org' | 'righttoken' | false
+ end
+
+ with_them do
+ subject { described_class.find_by_domain_and_token(domain_name, challenge_token).present? }
+
+ it { is_expected.to eq(present) }
+ end
+ end
+
+ subject { create(:pages_domain_acme_order) }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:pages_domain) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:pages_domain) }
+ it { is_expected.to validate_presence_of(:expires_at) }
+ it { is_expected.to validate_presence_of(:url) }
+ it { is_expected.to validate_presence_of(:challenge_token) }
+ it { is_expected.to validate_presence_of(:challenge_file_content) }
+ it { is_expected.to validate_presence_of(:private_key) }
+ end
+end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 4b85c5e8720..fdc81359d34 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PagesDomain do
@@ -79,6 +81,17 @@ describe PagesDomain do
end
end
+ describe 'when certificate is specified' do
+ let(:domain) { build(:pages_domain) }
+
+ it 'saves validity time' do
+ domain.save
+
+ expect(domain.certificate_valid_not_before).to be_like_time(Time.parse("2016-02-12 14:32:00 UTC"))
+ expect(domain.certificate_valid_not_after).to be_like_time(Time.parse("2020-04-12 14:32:00 UTC"))
+ end
+ end
+
describe 'validate certificate' do
subject { domain }
@@ -342,4 +355,32 @@ describe PagesDomain do
end
end
end
+
+ describe '.for_removal' do
+ subject { described_class.for_removal }
+
+ context 'when domain is not schedule for removal' do
+ let!(:domain) { create :pages_domain }
+
+ it 'does not return domain' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when domain is scheduled for removal yesterday' do
+ let!(:domain) { create :pages_domain, remove_at: 1.day.ago }
+
+ it 'returns domain' do
+ is_expected.to eq([domain])
+ end
+ end
+
+ context 'when domain is scheduled for removal tomorrow' do
+ let!(:domain) { create :pages_domain, remove_at: 1.day.from_now }
+
+ it 'does not return domain' do
+ is_expected.to be_empty
+ end
+ end
+ end
end
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index c82ab9c9e62..e0e1101ffc6 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PersonalAccessToken do
diff --git a/spec/models/pool_repository_spec.rb b/spec/models/pool_repository_spec.rb
index 112d4ab56fc..ae00f9df89e 100644
--- a/spec/models/pool_repository_spec.rb
+++ b/spec/models/pool_repository_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
describe PoolRepository do
describe 'associations' do
it { is_expected.to belong_to(:shard) }
- it { is_expected.to have_one(:source_project) }
+ it { is_expected.to belong_to(:source_project) }
it { is_expected.to have_many(:member_projects) }
end
@@ -24,14 +24,14 @@ describe PoolRepository do
end
end
- describe '#unlink_repository' do
+ describe '#mark_obsolete_if_last' do
let(:pool) { create(:pool_repository, :ready) }
context 'when the last member leaves' do
it 'schedules pool removal' do
expect(::ObjectPool::DestroyWorker).to receive(:perform_async).with(pool.id).and_call_original
- pool.unlink_repository(pool.source_project.repository)
+ pool.mark_obsolete_if_last(pool.source_project.repository)
end
end
@@ -40,7 +40,7 @@ describe PoolRepository do
create(:project, :repository, pool_repository: pool)
expect(::ObjectPool::DestroyWorker).not_to receive(:perform_async).with(pool.id)
- pool.unlink_repository(pool.source_project.repository)
+ pool.mark_obsolete_if_last(pool.source_project.repository)
end
end
end
diff --git a/spec/models/programming_language_spec.rb b/spec/models/programming_language_spec.rb
index 99cd358f863..b327d360461 100644
--- a/spec/models/programming_language_spec.rb
+++ b/spec/models/programming_language_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProgrammingLanguage do
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index c289ee0859a..6f06fe4e55a 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectAuthorization do
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index 7ff64c76e37..7bdd2367a68 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectAutoDevops do
@@ -12,65 +14,9 @@ describe ProjectAutoDevops do
it { is_expected.to respond_to(:created_at) }
it { is_expected.to respond_to(:updated_at) }
- describe '#has_domain?' do
- context 'when domain is defined' do
- let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: 'domain.com') }
-
- it { expect(auto_devops).to have_domain }
- end
-
- context 'when domain is empty' do
- let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: '') }
-
- context 'when there is an instance domain specified' do
- before do
- allow(Gitlab::CurrentSettings).to receive(:auto_devops_domain).and_return('example.com')
- end
-
- it { expect(auto_devops).to have_domain }
- end
-
- context 'when there is no instance domain specified' do
- before do
- allow(Gitlab::CurrentSettings).to receive(:auto_devops_domain).and_return(nil)
- end
-
- it { expect(auto_devops).not_to have_domain }
- end
- end
- end
-
describe '#predefined_variables' do
let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) }
- context 'when domain is defined' do
- let(:domain) { 'example.com' }
-
- it 'returns AUTO_DEVOPS_DOMAIN' do
- expect(auto_devops.predefined_variables).to include(domain_variable)
- end
- end
-
- context 'when domain is not defined' do
- let(:domain) { nil }
-
- context 'when there is an instance domain specified' do
- before do
- allow(Gitlab::CurrentSettings).to receive(:auto_devops_domain).and_return('example.com')
- end
-
- it { expect(auto_devops.predefined_variables).to include(domain_variable) }
- end
-
- context 'when there is no instance domain specified' do
- before do
- allow(Gitlab::CurrentSettings).to receive(:auto_devops_domain).and_return(nil)
- end
-
- it { expect(auto_devops.predefined_variables).not_to include(domain_variable) }
- end
- end
-
context 'when deploy_strategy is manual' do
let(:auto_devops) { build_stubbed(:project_auto_devops, :manual_deployment, project: project) }
let(:expected_variables) do
@@ -103,10 +49,6 @@ describe ProjectAutoDevops do
.not_to include("STAGING_ENABLED", "INCREMENTAL_ROLLOUT_ENABLED")
end
end
-
- def domain_variable
- { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }
- end
end
describe '#create_gitlab_deploy_token' do
@@ -115,7 +57,7 @@ describe ProjectAutoDevops do
context 'when the project is public' do
let(:project) { create(:project, :repository, :public) }
- it 'should not create a gitlab deploy token' do
+ it 'does not create a gitlab deploy token' do
expect do
auto_devops.save
end.not_to change { DeployToken.count }
@@ -125,7 +67,7 @@ describe ProjectAutoDevops do
context 'when the project is internal' do
let(:project) { create(:project, :repository, :internal) }
- it 'should create a gitlab deploy token' do
+ it 'creates a gitlab deploy token' do
expect do
auto_devops.save
end.to change { DeployToken.count }.by(1)
@@ -135,7 +77,7 @@ describe ProjectAutoDevops do
context 'when the project is private' do
let(:project) { create(:project, :repository, :private) }
- it 'should create a gitlab deploy token' do
+ it 'creates a gitlab deploy token' do
expect do
auto_devops.save
end.to change { DeployToken.count }.by(1)
@@ -146,7 +88,7 @@ describe ProjectAutoDevops do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
- it 'should create a deploy token' do
+ it 'creates a deploy token' do
expect do
auto_devops.save
end.to change { DeployToken.count }.by(1)
@@ -157,7 +99,7 @@ describe ProjectAutoDevops do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, enabled: nil, project: project) }
- it 'should create a deploy token' do
+ it 'creates a deploy token' do
allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true)
expect do
@@ -170,7 +112,7 @@ describe ProjectAutoDevops do
let(:project) { create(:project, :repository, :internal) }
let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) }
- it 'should not create a deploy token' do
+ it 'does not create a deploy token' do
expect do
auto_devops.save
end.not_to change { DeployToken.count }
@@ -182,7 +124,7 @@ describe ProjectAutoDevops do
let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, projects: [project]) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
- it 'should not create a deploy token' do
+ it 'does not create a deploy token' do
expect do
auto_devops.save
end.not_to change { DeployToken.count }
@@ -194,7 +136,7 @@ describe ProjectAutoDevops do
let!(:deploy_token) { create(:deploy_token, :gitlab_deploy_token, :expired, projects: [project]) }
let(:auto_devops) { build(:project_auto_devops, project: project) }
- it 'should not create a deploy token' do
+ it 'does not create a deploy token' do
expect do
auto_devops.save
end.not_to change { DeployToken.count }
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 4aa62028169..f596cee81dc 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -21,4 +21,32 @@ describe ProjectCiCdSetting do
2.times { described_class.available? }
end
end
+
+ describe 'validations' do
+ it 'validates default_git_depth is between 0 and 1000 or nil' do
+ expect(subject).to validate_numericality_of(:default_git_depth)
+ .only_integer
+ .is_greater_than_or_equal_to(0)
+ .is_less_than_or_equal_to(1000)
+ .allow_nil
+ end
+ end
+
+ describe '#default_git_depth' do
+ let(:default_value) { described_class::DEFAULT_GIT_DEPTH }
+
+ it 'sets default value for new records' do
+ project = create(:project)
+
+ expect(project.ci_cd_settings.default_git_depth).to eq(default_value)
+ end
+
+ it 'does not set default value if present' do
+ project = build(:project)
+ project.build_ci_cd_settings(default_git_depth: 0)
+ project.save!
+
+ expect(project.reload.ci_cd_settings.default_git_depth).to eq(0)
+ end
+ end
end
diff --git a/spec/models/project_custom_attribute_spec.rb b/spec/models/project_custom_attribute_spec.rb
index 669de5506bc..80638676b49 100644
--- a/spec/models/project_custom_attribute_spec.rb
+++ b/spec/models/project_custom_attribute_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectCustomAttribute do
diff --git a/spec/models/project_daily_statistic_spec.rb b/spec/models/project_daily_statistic_spec.rb
new file mode 100644
index 00000000000..86210af15d8
--- /dev/null
+++ b/spec/models/project_daily_statistic_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectDailyStatistic do
+ it { is_expected.to belong_to(:project) }
+end
diff --git a/spec/models/project_deploy_token_spec.rb b/spec/models/project_deploy_token_spec.rb
index 9e2e40c2e8f..2a5fefc1ab0 100644
--- a/spec/models/project_deploy_token_spec.rb
+++ b/spec/models/project_deploy_token_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe ProjectDeployToken, type: :model do
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index fee7d65c217..50c9d5968ac 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectFeature do
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 5bea21427d4..dad5506900b 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectGroupLink do
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index e3b2d971419..472bf8f9713 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe ProjectImportState, type: :model do
diff --git a/spec/models/project_label_spec.rb b/spec/models/project_label_spec.rb
index 689d4e505e5..330aab9f856 100644
--- a/spec/models/project_label_spec.rb
+++ b/spec/models/project_label_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectLabel do
diff --git a/spec/models/project_metrics_setting_spec.rb b/spec/models/project_metrics_setting_spec.rb
new file mode 100644
index 00000000000..7df01625ba1
--- /dev/null
+++ b/spec/models/project_metrics_setting_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ProjectMetricsSetting do
+ describe 'Associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'Validations' do
+ context 'when external_dashboard_url is over 255 chars' do
+ before do
+ subject.external_dashboard_url = 'https://' + 'a' * 250
+ end
+
+ it 'fails validation' do
+ expect(subject).not_to be_valid
+ expect(subject.errors.messages[:external_dashboard_url])
+ .to include('is too long (maximum is 255 characters)')
+ end
+ end
+
+ context 'with unsafe url' do
+ before do
+ subject.external_dashboard_url = %{https://replaceme.com/'><script>alert(document.cookie)</script>}
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'non ascii chars in external_dashboard_url' do
+ before do
+ subject.external_dashboard_url = 'http://gitlab.com/api/0/projects/project1/something€'
+ end
+
+ it { is_expected.to be_invalid }
+ end
+
+ context 'internal url in external_dashboard_url' do
+ before do
+ subject.external_dashboard_url = 'http://192.168.1.1'
+ end
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'external_dashboard_url is blank' do
+ before do
+ subject.external_dashboard_url = ''
+ end
+
+ it { is_expected.to be_invalid }
+ end
+ end
+end
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index e66109fd98f..18e839bcc64 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AsanaService do
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 5cb6d63659e..2c86c0ec7be 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AssemblaService do
+ include StubRequests
+
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -21,12 +25,12 @@ describe AssemblaService do
)
@sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
@api_url = 'https://atlas.assembla.com/spaces/project_name/github_tool?secret_key=verySecret'
- WebMock.stub_request(:post, @api_url)
+ stub_full_request(@api_url, method: :post)
end
it "calls Assembla API" do
@assembla_service.execute(@sample_data)
- expect(WebMock).to have_requested(:post, @api_url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(@api_url)).with(
body: /#{@sample_data[:before]}.*#{@sample_data[:after]}.*#{project.path}/
).once
end
diff --git a/spec/models/project_services/bamboo_service_spec.rb b/spec/models/project_services/bamboo_service_spec.rb
index b880d90d28f..65d227a17f9 100644
--- a/spec/models/project_services/bamboo_service_spec.rb
+++ b/spec/models/project_services/bamboo_service_spec.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BambooService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:bamboo_url) { 'http://gitlab.com/bamboo' }
@@ -255,7 +258,7 @@ describe BambooService, :use_clean_rails_memory_store_caching do
end
def stub_bamboo_request(url, status, body)
- WebMock.stub_request(:get, url).to_return(
+ stub_full_request(url).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb
index 43f7bcb1a19..6818db48fee 100644
--- a/spec/models/project_services/bugzilla_service_spec.rb
+++ b/spec/models/project_services/bugzilla_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BugzillaService do
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 1615a93a4ca..ca196069055 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildkiteService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:project) { create(:project) }
@@ -108,10 +111,9 @@ describe BuildkiteService, :use_clean_rails_memory_store_caching do
body ||= %q({"status":"success"})
buildkite_full_url = 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=123'
- WebMock.stub_request(:get, buildkite_full_url).to_return(
- status: status,
- headers: { 'Content-Type' => 'application/json' },
- body: body
- )
+ stub_full_request(buildkite_full_url)
+ .to_return(status: status,
+ headers: { 'Content-Type' => 'application/json' },
+ body: body)
end
end
diff --git a/spec/models/project_services/campfire_service_spec.rb b/spec/models/project_services/campfire_service_spec.rb
index ed8347edffd..0d3dd89e93b 100644
--- a/spec/models/project_services/campfire_service_spec.rb
+++ b/spec/models/project_services/campfire_service_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CampfireService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -47,39 +51,37 @@ describe CampfireService do
it "calls Campfire API to get a list of rooms and speak in a room" do
# make sure a valid list of rooms is returned
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms.json')
- WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
+
+ stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
body: body,
status: 200,
headers: @headers
)
+
# stub the speak request with the room id found in the previous request's response
speak_url = 'https://project-name.campfirenow.com/room/123/speak.json'
- WebMock.stub_request(:post, speak_url).with(basic_auth: @auth)
+ stub_full_request(speak_url, method: :post).with(basic_auth: @auth)
@campfire_service.execute(@sample_data)
- expect(WebMock).to have_requested(:get, @rooms_url).once
- expect(WebMock).to have_requested(:post, speak_url).with(
- body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/
- ).once
+ expect(WebMock).to have_requested(:get, stubbed_hostname(@rooms_url)).once
+ expect(WebMock).to have_requested(:post, stubbed_hostname(speak_url))
+ .with(body: /#{project.path}.*#{@sample_data[:before]}.*#{@sample_data[:after]}/).once
end
it "calls Campfire API to get a list of rooms but shouldn't speak in a room" do
# return a list of rooms that do not contain a room named 'test-room'
body = File.read(Rails.root + 'spec/fixtures/project_services/campfire/rooms2.json')
- WebMock.stub_request(:get, @rooms_url).with(basic_auth: @auth).to_return(
+ stub_full_request(@rooms_url).with(basic_auth: @auth).to_return(
body: body,
status: 200,
headers: @headers
)
- # we want to make sure no request is sent to the /speak endpoint, here is a basic
- # regexp that matches this endpoint
- speak_url = 'https://verySecret:X@project-name.campfirenow.com/room/.*/speak.json'
@campfire_service.execute(@sample_data)
- expect(WebMock).to have_requested(:get, @rooms_url).once
- expect(WebMock).not_to have_requested(:post, /#{speak_url}/)
+ expect(WebMock).to have_requested(:get, 'https://8.8.8.9/rooms.json').once
+ expect(WebMock).not_to have_requested(:post, '*/room/.*/speak.json')
end
end
end
diff --git a/spec/models/project_services/chat_message/deployment_message_spec.rb b/spec/models/project_services/chat_message/deployment_message_spec.rb
new file mode 100644
index 00000000000..42c1689db3d
--- /dev/null
+++ b/spec/models/project_services/chat_message/deployment_message_spec.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ChatMessage::DeploymentMessage do
+ describe '#pretext' do
+ it 'returns a message with the data returned by the deployment data builder' do
+ environment = create(:environment, name: "myenvironment")
+ project = create(:project, :repository)
+ commit = project.commit('HEAD')
+ deployment = create(:deployment, status: :success, environment: environment, project: project, sha: commit.sha)
+ data = Gitlab::DataBuilder::Deployment.build(deployment)
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq("Deploy to myenvironment succeeded")
+ end
+
+ it 'returns a message for a successful deployment' do
+ data = {
+ status: 'success',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production succeeded')
+ end
+
+ it 'returns a message for a failed deployment' do
+ data = {
+ status: 'failed',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production failed')
+ end
+
+ it 'returns a message for a canceled deployment' do
+ data = {
+ status: 'canceled',
+ environment: 'production'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to production canceled')
+ end
+
+ it 'returns a message for a deployment to another environment' do
+ data = {
+ status: 'success',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging succeeded')
+ end
+
+ it 'returns a message for a deployment with any other status' do
+ data = {
+ status: 'unknown',
+ environment: 'staging'
+ }
+
+ message = described_class.new(data)
+
+ expect(message.pretext).to eq('Deploy to staging unknown')
+ end
+ end
+
+ describe '#attachments' do
+ def deployment_data(params)
+ {
+ object_kind: "deployment",
+ status: "success",
+ deployable_id: 3,
+ deployable_url: "deployable_url",
+ environment: "sandbox",
+ project: {
+ name: "greatproject",
+ web_url: "project_web_url",
+ path_with_namespace: "project_path_with_namespace"
+ },
+ user: {
+ name: "Jane Person",
+ username: "jane"
+ },
+ user_url: "user_url",
+ short_sha: "12345678",
+ commit_url: "commit_url",
+ commit_title: "commit title text"
+ }.merge(params)
+ end
+
+ it 'returns attachments with the data returned by the deployment data builder' do
+ user = create(:user, name: "John Smith", username: "smith")
+ namespace = create(:namespace, name: "myspace")
+ project = create(:project, :repository, namespace: namespace, name: "myproject")
+ commit = project.commit('HEAD')
+ environment = create(:environment, name: "myenvironment", project: project)
+ ci_build = create(:ci_build, project: project)
+ deployment = create(:deployment, :success, deployable: ci_build, environment: environment, project: project, user: user, sha: commit.sha)
+ job_url = Gitlab::Routing.url_helpers.project_job_url(project, ci_build)
+ commit_url = Gitlab::UrlBuilder.build(deployment.commit)
+ user_url = Gitlab::Routing.url_helpers.user_url(user)
+ data = Gitlab::DataBuilder::Deployment.build(deployment)
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[myspace/myproject](#{project.web_url}) with job [##{ci_build.id}](#{job_url}) by [John Smith (smith)](#{user_url})\n[#{deployment.short_sha}](#{commit_url}): #{commit.title}",
+ color: "good"
+ }])
+ end
+
+ it 'returns attachments for a failed deployment' do
+ data = deployment_data(status: 'failed')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "danger"
+ }])
+ end
+
+ it 'returns attachments for a canceled deployment' do
+ data = deployment_data(status: 'canceled')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "warning"
+ }])
+ end
+
+ it 'uses a neutral color for a deployment with any other status' do
+ data = deployment_data(status: 'some-new-status-we-make-in-the-future')
+
+ message = described_class.new(data)
+
+ expect(message.attachments).to eq([{
+ text: "[project_path_with_namespace](project_web_url) with job [#3](deployable_url) by [Jane Person (jane)](user_url)\n[12345678](commit_url): commit title text",
+ color: "#334455"
+ }])
+ end
+ end
+end
diff --git a/spec/models/project_services/chat_message/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
index f7a35fdc88a..d3adc62c38e 100644
--- a/spec/models/project_services/chat_message/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatMessage::IssueMessage do
diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
index 7997b5bb6b9..b56eb19dd55 100644
--- a/spec/models/project_services/chat_message/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatMessage::MergeMessage do
diff --git a/spec/models/project_services/chat_message/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
index 5abbd7bec18..5e7987dc0f6 100644
--- a/spec/models/project_services/chat_message/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatMessage::NoteMessage do
diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
index 0ff20400999..8f9fa310ad4 100644
--- a/spec/models/project_services/chat_message/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatMessage::PipelineMessage do
diff --git a/spec/models/project_services/chat_message/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
index 973d6bdb2a0..a89645a3ea8 100644
--- a/spec/models/project_services/chat_message/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatMessage::PushMessage do
diff --git a/spec/models/project_services/chat_message/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
index 7efcba9bcfd..c3db516f253 100644
--- a/spec/models/project_services/chat_message/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatMessage::WikiPageMessage do
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
index 46713df77da..6f4ddd223f6 100644
--- a/spec/models/project_services/chat_notification_service_spec.rb
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatNotificationService do
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index 7e1b1a4f2af..f0e7551693d 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CustomIssueTrackerService do
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index 26597d9b83c..22df19d943f 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DroneCiService, :use_clean_rails_memory_store_caching do
diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb
index d9b7010e5e5..0a58eb367e3 100644
--- a/spec/models/project_services/emails_on_push_service_spec.rb
+++ b/spec/models/project_services/emails_on_push_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EmailsOnPushService do
diff --git a/spec/models/project_services/external_wiki_service_spec.rb b/spec/models/project_services/external_wiki_service_spec.rb
index 62fd97b038b..bdd8605436f 100644
--- a/spec/models/project_services/external_wiki_service_spec.rb
+++ b/spec/models/project_services/external_wiki_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ExternalWikiService do
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index fabcb142858..c1ebe69ee66 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe FlowdockService do
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 3237b660a16..11f96c03d46 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabIssueTrackerService do
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
new file mode 100644
index 00000000000..a04b984c1f6
--- /dev/null
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -0,0 +1,409 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe HipchatService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:token) }
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:token) }
+ end
+ end
+
+ describe "Execute" do
+ let(:hipchat) { described_class.new }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' }
+ let(:project_name) { project.full_name.gsub(/\s/, '') }
+ let(:token) { 'verySecret' }
+ let(:server_url) { 'https://hipchat.example.com'}
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build_sample(project, user)
+ end
+
+ before do
+ allow(hipchat).to receive_messages(
+ project_id: project.id,
+ project: project,
+ room: 123456,
+ server: server_url,
+ token: token
+ )
+ WebMock.stub_request(:post, api_url)
+ end
+
+ it 'tests and return errors' do
+ allow(hipchat).to receive(:execute).and_raise(StandardError, 'no such room')
+ result = hipchat.test(push_sample_data)
+
+ expect(result[:success]).to be_falsey
+ expect(result[:result].to_s).to eq('no such room')
+ end
+
+ it 'uses v1 if version is provided' do
+ allow(hipchat).to receive(:api_version).and_return('v1')
+ expect(HipChat::Client).to receive(:new).with(
+ token,
+ api_version: 'v1',
+ server_url: server_url
+ ).and_return(double(:hipchat_service).as_null_object)
+ hipchat.execute(push_sample_data)
+ end
+
+ it 'uses v2 as the version when nothing is provided' do
+ allow(hipchat).to receive(:api_version).and_return('')
+ expect(HipChat::Client).to receive(:new).with(
+ token,
+ api_version: 'v2',
+ server_url: server_url
+ ).and_return(double(:hipchat_service).as_null_object)
+ hipchat.execute(push_sample_data)
+ end
+
+ context 'push events' do
+ it "calls Hipchat API for push events" do
+ hipchat.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates a push message" do
+ message = hipchat.send(:create_push_message, push_sample_data)
+
+ push_sample_data[:object_attributes]
+ branch = push_sample_data[:ref].gsub('refs/heads/', '')
+ expect(message).to include("#{user.name} pushed to branch " \
+ "<a href=\"#{project.web_url}/commits/#{branch}\">#{branch}</a> of " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>")
+ end
+ end
+
+ context 'tag_push events' do
+ let(:push_sample_data) do
+ Gitlab::DataBuilder::Push.build(
+ project: project,
+ user: user,
+ oldrev: Gitlab::Git::BLANK_SHA,
+ newrev: '1' * 40,
+ ref: 'refs/tags/test')
+ end
+
+ it "calls Hipchat API for tag push events" do
+ hipchat.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates a tag push message" do
+ message = hipchat.send(:create_push_message, push_sample_data)
+
+ push_sample_data[:object_attributes]
+ expect(message).to eq("#{user.name} pushed new tag " \
+ "<a href=\"#{project.web_url}/commits/test\">test</a> to " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>\n")
+ end
+ end
+
+ context 'issue events' do
+ let(:issue) { create(:issue, title: 'Awesome issue', description: '**please** fix') }
+ let(:issue_service) { Issues::CreateService.new(project, user) }
+ let(:issues_sample_data) { issue_service.hook_data(issue, 'open') }
+
+ it "calls Hipchat API for issue events" do
+ hipchat.execute(issues_sample_data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates an issue message" do
+ message = hipchat.send(:create_issue_message, issues_sample_data)
+
+ obj_attr = issues_sample_data[:object_attributes]
+ expect(message).to eq("#{user.name} opened " \
+ "<a href=\"#{obj_attr[:url]}\">issue ##{obj_attr["iid"]}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>Awesome issue</b>" \
+ "<pre><strong>please</strong> fix</pre>")
+ end
+ end
+
+ context 'merge request events' do
+ let(:merge_request) { create(:merge_request, description: '**please** fix', title: 'Awesome merge request', target_project: project, source_project: project) }
+ let(:merge_service) { MergeRequests::CreateService.new(project, user) }
+ let(:merge_sample_data) { merge_service.hook_data(merge_request, 'open') }
+
+ it "calls Hipchat API for merge requests events" do
+ hipchat.execute(merge_sample_data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates a merge request message" do
+ message = hipchat.send(:create_merge_request_message,
+ merge_sample_data)
+
+ obj_attr = merge_sample_data[:object_attributes]
+ expect(message).to eq("#{user.name} opened " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>Awesome merge request</b>" \
+ "<pre><strong>please</strong> fix</pre>")
+ end
+ end
+
+ context "Note events" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository, creator: user) }
+
+ context 'when commit comment event triggered' do
+ let(:commit_note) do
+ create(:note_on_commit, author: user, project: project,
+ commit_id: project.repository.commit.id,
+ note: 'a comment on a commit')
+ end
+
+ it "calls Hipchat API for commit comment events" do
+ data = Gitlab::DataBuilder::Note.build(commit_note, user)
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ commit_id = Commit.truncate_sha(data[:commit][:id])
+ title = hipchat.send(:format_title, data[:commit][:message])
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">commit #{commit_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "#{title}" \
+ "<pre>a comment on a commit</pre>")
+ end
+ end
+
+ context 'when merge request comment event triggered' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project,
+ target_project: project)
+ end
+
+ let(:merge_request_note) do
+ create(:note_on_merge_request, noteable: merge_request,
+ project: project,
+ note: "merge request **note**")
+ end
+
+ it "calls Hipchat API for merge request comment events" do
+ data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ merge_id = data[:merge_request]['iid']
+ title = data[:merge_request]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>merge request <strong>note</strong></pre>")
+ end
+ end
+
+ context 'when issue comment event triggered' do
+ let(:issue) { create(:issue, project: project) }
+ let(:issue_note) do
+ create(:note_on_issue, noteable: issue, project: project,
+ note: "issue **note**")
+ end
+
+ it "calls Hipchat API for issue comment events" do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ hipchat.execute(data)
+
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ issue_id = data[:issue]['iid']
+ title = data[:issue]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">issue ##{issue_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>issue <strong>note</strong></pre>")
+ end
+
+ context 'with confidential issue' do
+ before do
+ issue.update!(confidential: true)
+ end
+
+ it 'calls Hipchat API with issue comment' do
+ data = Gitlab::DataBuilder::Note.build(issue_note, user)
+ hipchat.execute(data)
+
+ message = hipchat.send(:create_message, data)
+
+ expect(message).to include("<pre>issue <strong>note</strong></pre>")
+ end
+ end
+ end
+
+ context 'when snippet comment event triggered' do
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:snippet_note) do
+ create(:note_on_project_snippet, noteable: snippet,
+ project: project,
+ note: "snippet note")
+ end
+
+ it "calls Hipchat API for snippet comment events" do
+ data = Gitlab::DataBuilder::Note.build(snippet_note, user)
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+
+ message = hipchat.send(:create_message, data)
+
+ obj_attr = data[:object_attributes]
+ snippet_id = data[:snippet]['id']
+ title = data[:snippet]['title']
+
+ expect(message).to eq("#{user.name} commented on " \
+ "<a href=\"#{obj_attr[:url]}\">snippet ##{snippet_id}</a> in " \
+ "<a href=\"#{project.web_url}\">#{project_name}</a>: " \
+ "<b>#{title}</b>" \
+ "<pre>snippet note</pre>")
+ end
+ end
+ end
+
+ context 'pipeline events' do
+ let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) }
+ let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
+
+ context 'for failed' do
+ before do
+ pipeline.drop
+ end
+
+ it "calls Hipchat API" do
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "creates a build message" do
+ message = hipchat.__send__(:create_pipeline_message, data)
+
+ project_url = project.web_url
+ project_name = project.full_name.gsub(/\s/, '')
+ pipeline_attributes = data[:object_attributes]
+ ref = pipeline_attributes[:ref]
+ ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ duration = pipeline_attributes[:duration]
+ user_name = data[:user][:name]
+
+ expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
+ "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \
+ "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
+ "by #{user_name} failed in #{duration} second(s)")
+ end
+ end
+
+ context 'for succeeded' do
+ before do
+ pipeline.succeed
+ end
+
+ it "calls Hipchat API" do
+ hipchat.notify_only_broken_pipelines = false
+ hipchat.execute(data)
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "notifies only broken" do
+ hipchat.notify_only_broken_pipelines = true
+ hipchat.execute(data)
+ expect(WebMock).not_to have_requested(:post, api_url).once
+ end
+ end
+ end
+
+ context "#message_options" do
+ it "is set to the defaults" do
+ expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' })
+ end
+
+ it "sets notify to true" do
+ allow(hipchat).to receive(:notify).and_return('1')
+
+ expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' })
+ end
+
+ it "sets the color" do
+ allow(hipchat).to receive(:color).and_return('red')
+
+ expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' })
+ end
+
+ context 'with a successful build' do
+ it 'uses the green color' do
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'success' } }
+
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
+ end
+ end
+
+ context 'with a failed build' do
+ it 'uses the red color' do
+ data = { object_kind: 'pipeline',
+ object_attributes: { status: 'failed' } }
+
+ expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
+ end
+ end
+ end
+ end
+
+ context 'with UrlBlocker' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:hipchat) { create(:hipchat_service, project: project, properties: { room: 'test' }) }
+ let(:push_sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
+
+ describe '#execute' do
+ before do
+ hipchat.server = 'http://localhost:9123'
+ end
+
+ it 'raises UrlBlocker for localhost' do
+ expect(Gitlab::UrlBlocker).to receive(:validate!).and_call_original
+ expect { hipchat.execute(push_sample_data) }.to raise_error(Gitlab::HTTP::BlockedUrlError)
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index cb9ca76fc3f..2e1f6964692 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'socket'
require 'json'
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
index e6a1752576b..2fc4d69c2db 100644
--- a/spec/models/project_services/issue_tracker_service_spec.rb
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssueTrackerService do
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 788b3179b01..04ae9390436 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe JiraService do
@@ -164,6 +166,13 @@ describe JiraService do
).once
end
+ it 'does not fail if remote_link.all on issue returns nil' do
+ allow(JIRA::Resource::Remotelink).to receive(:all).and_return(nil)
+
+ expect { @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project)) }
+ .not_to raise_error(NoMethodError)
+ end
+
# Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
# for more information
it 'creates Remote Link reference in JIRA for comment' do
@@ -177,9 +186,10 @@ describe JiraService do
expect(WebMock).to have_requested(:post, @remote_link_url).with(
body: hash_including(
GlobalID: 'GitLab',
+ relationship: 'mentioned on',
object: {
url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{commit_id}",
- title: "GitLab: Solved by commit #{commit_id}.",
+ title: "Solved by commit #{commit_id}.",
icon: { title: 'GitLab', url16x16: favicon_path },
status: { resolved: true }
}
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
index 9c27357ffaf..2fce120381b 100644
--- a/spec/models/project_services/kubernetes_service_spec.rb
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe KubernetesService, :use_clean_rails_memory_store_caching do
@@ -68,11 +70,11 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
kubernetes_service.properties['namespace'] = "foo"
end
- it 'should not update attributes' do
+ it 'does not update attributes' do
expect(kubernetes_service.save).to be_falsy
end
- it 'should include an error with a deprecation message' do
+ it 'includes an error with a deprecation message' do
kubernetes_service.valid?
expect(kubernetes_service.errors[:base].first).to match(/Kubernetes service integration has been deprecated/)
end
@@ -81,7 +83,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
context 'with a non-deprecated service' do
let(:kubernetes_service) { create(:kubernetes_service) }
- it 'should update attributes' do
+ it 'updates attributes' do
kubernetes_service.properties['namespace'] = 'foo'
expect(kubernetes_service.save).to be_truthy
end
@@ -96,15 +98,15 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
kubernetes_service.save
end
- it 'should deactive the service' do
+ it 'deactivates the service' do
expect(kubernetes_service.active?).to be_falsy
end
- it 'should not include a deprecation message as error' do
+ it 'does not include a deprecation message as error' do
expect(kubernetes_service.errors.messages.count).to eq(0)
end
- it 'should update attributes' do
+ it 'updates attributes' do
expect(kubernetes_service.properties['namespace']).to eq("foo")
end
end
@@ -116,7 +118,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
kubernetes_service.properties['namespace'] = 'foo'
end
- it 'should update attributes' do
+ it 'updates attributes' do
expect(kubernetes_service.save).to be_truthy
expect(kubernetes_service.properties['namespace']).to eq('foo')
end
@@ -159,8 +161,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
end
end
- describe '#actual_namespace' do
- subject { service.actual_namespace }
+ describe '#kubernetes_namespace_for' do
+ subject { service.kubernetes_namespace_for(project) }
shared_examples 'a correctly formatted namespace' do
it 'returns a valid Kubernetes namespace name' do
@@ -276,7 +278,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
it 'sets the variables' do
expect(subject.predefined_variables(project: project)).to include(
{ key: 'KUBE_URL', value: 'https://kube.domain.com', public: true },
- { key: 'KUBE_TOKEN', value: 'token', public: false },
+ { key: 'KUBE_TOKEN', value: 'token', public: false, masked: true },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true },
{ key: 'KUBECONFIG', value: kubeconfig, public: false, file: true },
{ key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true },
@@ -296,7 +298,7 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
end
context 'no namespace provided' do
- let(:namespace) { subject.actual_namespace }
+ let(:namespace) { subject.kubernetes_namespace_for(project) }
it_behaves_like 'setting variables'
@@ -323,13 +325,14 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
end
context 'with valid pods' do
- let(:pod) { kube_pod(app: environment.slug) }
+ let(:pod) { kube_pod(environment_slug: environment.slug, namespace: service.kubernetes_namespace_for(project), project_slug: project.full_path_slug) }
+ let(:pod_with_no_terminal) { kube_pod(environment_slug: environment.slug, project_slug: project.full_path_slug, status: "Pending") }
let(:terminals) { kube_terminals(service, pod) }
before do
stub_reactive_cache(
service,
- pods: [pod, pod, kube_pod(app: "should-be-filtered-out")]
+ pods: [pod, pod, pod_with_no_terminal, kube_pod(environment_slug: "should-be-filtered-out")]
)
end
@@ -349,6 +352,8 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
describe '#calculate_reactive_cache' do
subject { service.calculate_reactive_cache }
+ let(:namespace) { service.kubernetes_namespace_for(project) }
+
context 'when service is inactive' do
before do
service.active = false
@@ -359,15 +364,17 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
context 'when kubernetes responds with valid pods' do
before do
- stub_kubeclient_pods
+ stub_kubeclient_pods(namespace)
+ stub_kubeclient_deployments(namespace) # Used by EE
end
- it { is_expected.to eq(pods: [kube_pod]) }
+ it { is_expected.to include(pods: [kube_pod]) }
end
context 'when kubernetes responds with 500s' do
before do
- stub_kubeclient_pods(status: 500)
+ stub_kubeclient_pods(namespace, status: 500)
+ stub_kubeclient_deployments(namespace, status: 500) # Used by EE
end
it { expect { subject }.to raise_error(Kubeclient::HttpError) }
@@ -375,10 +382,11 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
context 'when kubernetes responds with 404s' do
before do
- stub_kubeclient_pods(status: 404)
+ stub_kubeclient_pods(namespace, status: 404)
+ stub_kubeclient_deployments(namespace, status: 404) # Used by EE
end
- it { is_expected.to eq(pods: []) }
+ it { is_expected.to include(pods: []) }
end
end
@@ -386,13 +394,13 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
let(:kubernetes_service) { create(:kubernetes_service) }
context 'with an active kubernetes service' do
- it 'should return false' do
+ it 'returns false' do
expect(kubernetes_service.deprecated?).to be_falsy
end
end
context 'with a inactive kubernetes service' do
- it 'should return true' do
+ it 'returns true' do
kubernetes_service.update_attribute(:active, false)
expect(kubernetes_service.deprecated?).to be_truthy
end
@@ -402,18 +410,18 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do
describe "#deprecation_message" do
let(:kubernetes_service) { create(:kubernetes_service) }
- it 'should indicate the service is deprecated' do
+ it 'indicates the service is deprecated' do
expect(kubernetes_service.deprecation_message).to match(/Kubernetes service integration has been deprecated/)
end
context 'if the services is active' do
- it 'should return a message' do
+ it 'returns a message' do
expect(kubernetes_service.deprecation_message).to match(/Your Kubernetes cluster information on this page is still editable/)
end
end
context 'if the service is not active' do
- it 'should return a message' do
+ it 'returns a message' do
kubernetes_service.update_attribute(:active, false)
expect(kubernetes_service.deprecation_message).to match(/Fields on this page are now uneditable/)
end
diff --git a/spec/models/project_services/mattermost_service_spec.rb b/spec/models/project_services/mattermost_service_spec.rb
index 10c62ca55a7..6261c70f266 100644
--- a/spec/models/project_services/mattermost_service_spec.rb
+++ b/spec/models/project_services/mattermost_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MattermostService do
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 1983e0cc967..87e482059f2 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MattermostSlashCommandsService do
diff --git a/spec/models/project_services/microsoft_teams_service_spec.rb b/spec/models/project_services/microsoft_teams_service_spec.rb
index 3351c6280b4..c025d7c882e 100644
--- a/spec/models/project_services/microsoft_teams_service_spec.rb
+++ b/spec/models/project_services/microsoft_teams_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MicrosoftTeamsService do
@@ -28,6 +30,12 @@ describe MicrosoftTeamsService do
end
end
+ describe '.supported_events' do
+ it 'does not support deployment_events' do
+ expect(described_class.supported_events).not_to include('deployment')
+ end
+ end
+
describe "#execute" do
let(:user) { create(:user) }
set(:project) { create(:project, :repository, :wiki_repo) }
diff --git a/spec/models/project_services/packagist_service_spec.rb b/spec/models/project_services/packagist_service_spec.rb
index 6acee311700..53f18a1bdd9 100644
--- a/spec/models/project_services/packagist_service_spec.rb
+++ b/spec/models/project_services/packagist_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PackagistService do
diff --git a/spec/models/project_services/pipelines_email_service_spec.rb b/spec/models/project_services/pipelines_email_service_spec.rb
index 75ae2207910..b85565e0c25 100644
--- a/spec/models/project_services/pipelines_email_service_spec.rb
+++ b/spec/models/project_services/pipelines_email_service_spec.rb
@@ -1,8 +1,14 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelinesEmailService, :mailer do
let(:pipeline) do
- create(:ci_pipeline, project: project, sha: project.commit('master').sha)
+ create(:ci_pipeline, :failed,
+ project: project,
+ sha: project.commit('master').sha,
+ ref: project.default_branch
+ )
end
let(:project) { create(:project, :repository) }
@@ -82,12 +88,7 @@ describe PipelinesEmailService, :mailer do
subject.test(data)
end
- context 'when pipeline is failed' do
- before do
- data[:object_attributes][:status] = 'failed'
- pipeline.update(status: 'failed')
- end
-
+ context 'when pipeline is failed and on default branch' do
it_behaves_like 'sending email'
end
@@ -99,6 +100,25 @@ describe PipelinesEmailService, :mailer do
it_behaves_like 'sending email'
end
+
+ context 'when pipeline is failed and on a non-default branch' do
+ before do
+ data[:object_attributes][:ref] = 'not-the-default-branch'
+ pipeline.update(ref: 'not-the-default-branch')
+ end
+
+ context 'with notify_only_default branch on' do
+ before do
+ subject.notify_only_default_branch = true
+ end
+
+ it_behaves_like 'sending email'
+ end
+
+ context 'with notify_only_default_branch off' do
+ it_behaves_like 'sending email'
+ end
+ end
end
describe '#execute' do
@@ -108,11 +128,6 @@ describe PipelinesEmailService, :mailer do
context 'with recipients' do
context 'with failed pipeline' do
- before do
- data[:object_attributes][:status] = 'failed'
- pipeline.update(status: 'failed')
- end
-
it_behaves_like 'sending email'
end
@@ -131,11 +146,6 @@ describe PipelinesEmailService, :mailer do
end
context 'with failed pipeline' do
- before do
- data[:object_attributes][:status] = 'failed'
- pipeline.update(status: 'failed')
- end
-
it_behaves_like 'sending email'
end
@@ -148,6 +158,40 @@ describe PipelinesEmailService, :mailer do
it_behaves_like 'not sending email'
end
end
+
+ context 'with notify_only_default_branch off' do
+ context 'with default branch' do
+ it_behaves_like 'sending email'
+ end
+
+ context 'with non default branch' do
+ before do
+ data[:object_attributes][:ref] = 'not-the-default-branch'
+ pipeline.update(ref: 'not-the-default-branch')
+ end
+
+ it_behaves_like 'sending email'
+ end
+ end
+
+ context 'with notify_only_default_branch on' do
+ before do
+ subject.notify_only_default_branch = true
+ end
+
+ context 'with default branch' do
+ it_behaves_like 'sending email'
+ end
+
+ context 'with non default branch' do
+ before do
+ data[:object_attributes][:ref] = 'not-the-default-branch'
+ pipeline.update(ref: 'not-the-default-branch')
+ end
+
+ it_behaves_like 'not sending email'
+ end
+ end
end
context 'with empty recipients list' do
diff --git a/spec/models/project_services/pivotaltracker_service_spec.rb b/spec/models/project_services/pivotaltracker_service_spec.rb
index f7d2372eca2..dde46c82df6 100644
--- a/spec/models/project_services/pivotaltracker_service_spec.rb
+++ b/spec/models/project_services/pivotaltracker_service_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PivotaltrackerService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -51,12 +55,12 @@ describe PivotaltrackerService do
end
before do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
end
- it 'should post correct message' do
+ it 'posts correct message' do
service.execute(push_data)
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
body: {
'source_commit' => {
'commit_id' => '21c12ea',
@@ -79,18 +83,18 @@ describe PivotaltrackerService do
end
end
- it 'should post message if branch is in the list' do
+ it 'posts message if branch is in the list' do
service.execute(push_data(branch: 'master'))
service.execute(push_data(branch: 'v10'))
- expect(WebMock).to have_requested(:post, url).twice
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).twice
end
- it 'should not post message if branch is not in the list' do
+ it 'does not post message if branch is not in the list' do
service.execute(push_data(branch: 'mas'))
service.execute(push_data(branch: 'v11'))
- expect(WebMock).not_to have_requested(:post, url)
+ expect(WebMock).not_to have_requested(:post, stubbed_hostname(url))
end
end
end
diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb
index b6cf4c72450..e9c7c94ad70 100644
--- a/spec/models/project_services/prometheus_service_spec.rb
+++ b/spec/models/project_services/prometheus_service_spec.rb
@@ -33,18 +33,38 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
describe 'Validations' do
context 'when manual_configuration is enabled' do
before do
- subject.manual_configuration = true
+ service.manual_configuration = true
end
- it { is_expected.to validate_presence_of(:api_url) }
+ it 'validates presence of api_url' do
+ expect(service).to validate_presence_of(:api_url)
+ end
end
context 'when manual configuration is disabled' do
before do
- subject.manual_configuration = false
+ service.manual_configuration = false
end
- it { is_expected.not_to validate_presence_of(:api_url) }
+ it 'does not validate presence of api_url' do
+ expect(service).not_to validate_presence_of(:api_url)
+ end
+ end
+
+ context 'when the api_url domain points to localhost or local network' do
+ let(:domain) { Addressable::URI.parse(service.api_url).hostname }
+
+ it 'cannot query' do
+ expect(service.can_query?).to be true
+
+ aggregate_failures do
+ ['127.0.0.1', '192.168.2.3'].each do |url|
+ allow(Addrinfo).to receive(:getaddrinfo).with(domain, any_args).and_return([Addrinfo.tcp(url, 80)])
+
+ expect(service.can_query?).to be false
+ end
+ end
+ end
end
end
@@ -74,30 +94,35 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
end
describe '#prometheus_client' do
+ let(:api_url) { 'http://some_url' }
+
+ before do
+ service.active = true
+ service.api_url = api_url
+ service.manual_configuration = manual_configuration
+ end
+
context 'manual configuration is enabled' do
- let(:api_url) { 'http://some_url' }
+ let(:manual_configuration) { true }
- before do
- subject.active = true
- subject.manual_configuration = true
- subject.api_url = api_url
+ it 'returns rest client from api_url' do
+ expect(service.prometheus_client.url).to eq(api_url)
end
- it 'returns rest client from api_url' do
- expect(subject.prometheus_client.url).to eq(api_url)
+ it 'calls valid?' do
+ allow(service).to receive(:valid?).and_call_original
+
+ expect(service.prometheus_client).not_to be_nil
+
+ expect(service).to have_received(:valid?)
end
end
context 'manual configuration is disabled' do
- let(:api_url) { 'http://some_url' }
-
- before do
- subject.manual_configuration = false
- subject.api_url = api_url
- end
+ let(:manual_configuration) { false }
it 'no client provided' do
- expect(subject.prometheus_client).to be_nil
+ expect(service.prometheus_client).to be_nil
end
end
end
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 54b8c658ff6..380f02739bc 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PushoverService do
+ include StubRequests
+
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -55,13 +59,13 @@ describe PushoverService do
sound: sound
)
- WebMock.stub_request(:post, api_url)
+ stub_full_request(api_url, method: :post, ip_address: '8.8.8.8')
end
it 'calls Pushover API' do
pushover.execute(sample_data)
- expect(WebMock).to have_requested(:post, api_url).once
+ expect(WebMock).to have_requested(:post, 'https://8.8.8.8/1/messages.json').once
end
end
end
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index 2ac14eab5e1..ac570ac27e1 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RedmineService do
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 13cf4d1915e..01f580c5d01 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SlackService do
diff --git a/spec/models/project_services/slack_slash_commands_service_spec.rb b/spec/models/project_services/slack_slash_commands_service_spec.rb
index 5c4bce90ace..8c57907d064 100644
--- a/spec/models/project_services/slack_slash_commands_service_spec.rb
+++ b/spec/models/project_services/slack_slash_commands_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SlackSlashCommandsService do
diff --git a/spec/models/project_services/teamcity_service_spec.rb b/spec/models/project_services/teamcity_service_spec.rb
index 64b4efca43a..1c434b25205 100644
--- a/spec/models/project_services/teamcity_service_spec.rb
+++ b/spec/models/project_services/teamcity_service_spec.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TeamcityService, :use_clean_rails_memory_store_caching do
include ReactiveCachingHelpers
+ include StubRequests
let(:teamcity_url) { 'http://gitlab.com/teamcity' }
@@ -210,7 +213,7 @@ describe TeamcityService, :use_clean_rails_memory_store_caching do
body ||= %Q({"build":{"status":"#{build_status}","id":"666"}})
- WebMock.stub_request(:get, teamcity_full_url).with(basic_auth: auth).to_return(
+ stub_full_request(teamcity_full_url).with(basic_auth: auth).to_return(
status: status,
headers: { 'Content-Type' => 'application/json' },
body: body
diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb
new file mode 100644
index 00000000000..bf9d892f66c
--- /dev/null
+++ b/spec/models/project_services/youtrack_service_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe YoutrackService do
+ describe 'Associations' do
+ it { is_expected.to belong_to :project }
+ it { is_expected.to have_one :service_hook }
+ end
+
+ describe 'Validations' do
+ context 'when service is active' do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of(:project_url) }
+ it { is_expected.to validate_presence_of(:issues_url) }
+ it_behaves_like 'issue tracker service URL attribute', :project_url
+ it_behaves_like 'issue tracker service URL attribute', :issues_url
+ end
+
+ context 'when service is inactive' do
+ before do
+ subject.active = false
+ end
+
+ it { is_expected.not_to validate_presence_of(:project_url) }
+ it { is_expected.not_to validate_presence_of(:issues_url) }
+ end
+ end
+
+ describe '.reference_pattern' do
+ it_behaves_like 'allows project key on reference pattern'
+
+ it 'does allow project prefix on the reference' do
+ expect(described_class.reference_pattern.match('YT-123')[:issue]).to eq('YT-123')
+ end
+ end
+end
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index 1b439bcfad1..e87b4f41f4d 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectSnippet do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index bcbe687f4a2..aad08b9d4aa 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1,8 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Project do
include ProjectForksHelper
include GitHelpers
+ include ExternalAuthorizationServiceHelpers
it_behaves_like 'having unique enum values'
@@ -41,6 +44,7 @@ describe Project do
it { is_expected.to have_one(:pipelines_email_service) }
it { is_expected.to have_one(:irker_service) }
it { is_expected.to have_one(:pivotaltracker_service) }
+ it { is_expected.to have_one(:hipchat_service) }
it { is_expected.to have_one(:flowdock_service) }
it { is_expected.to have_one(:assembla_service) }
it { is_expected.to have_one(:slack_slash_commands_service) }
@@ -50,6 +54,7 @@ describe Project do
it { is_expected.to have_one(:teamcity_service) }
it { is_expected.to have_one(:jira_service) }
it { is_expected.to have_one(:redmine_service) }
+ it { is_expected.to have_one(:youtrack_service) }
it { is_expected.to have_one(:custom_issue_tracker_service) }
it { is_expected.to have_one(:bugzilla_service) }
it { is_expected.to have_one(:gitlab_issue_tracker_service) }
@@ -135,15 +140,6 @@ describe Project do
end
end
- describe '#boards' do
- it 'raises an error when attempting to add more than one board to the project' do
- subject.boards.build
-
- expect { subject.boards.build }.to raise_error(Project::BoardLimitExceeded, 'Number of permitted boards exceeded')
- expect(subject.boards.size).to eq 1
- end
- end
-
describe 'ci_pipelines association' do
it 'returns only pipelines from ci_sources' do
expect(Ci::Pipeline).to receive(:ci_sources).and_call_original
@@ -218,6 +214,13 @@ describe Project do
expect(project2).not_to be_valid
end
+ it 'validates the visibility' do
+ expect_any_instance_of(described_class).to receive(:visibility_level_allowed_as_fork).and_call_original
+ expect_any_instance_of(described_class).to receive(:visibility_level_allowed_by_group).and_call_original
+
+ create(:project)
+ end
+
describe 'wiki path conflict' do
context "when the new path has been used by the wiki of other Project" do
it 'has an error on the name attribute' do
@@ -421,7 +424,7 @@ describe Project do
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end
- it 'should return .external pipelines' do
+ it 'returns .external pipelines' do
expect(project.all_pipelines).to all(have_attributes(source: 'external'))
expect(project.all_pipelines.size).to eq(1)
end
@@ -445,7 +448,7 @@ describe Project do
project.project_feature.update_attribute(:builds_access_level, ProjectFeature::DISABLED)
end
- it 'should return .external pipelines' do
+ it 'returns .external pipelines' do
expect(project.ci_pipelines).to all(have_attributes(source: 'external'))
expect(project.ci_pipelines.size).to eq(1)
end
@@ -1144,7 +1147,7 @@ describe Project do
allow(project).to receive(:avatar_in_git) { true }
end
- let(:avatar_path) { "/#{project.full_path}/avatar" }
+ let(:avatar_path) { "/#{project.full_path}/-/avatar" }
it { is_expected.to eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
end
@@ -1916,7 +1919,7 @@ describe Project do
tags: %w[latest rc1])
end
- it 'should have image tags' do
+ it 'has image tags' do
expect(project).to have_container_registry_tags
end
end
@@ -1927,7 +1930,7 @@ describe Project do
tags: %w[latest rc1 pre1])
end
- it 'should have image tags' do
+ it 'has image tags' do
expect(project).to have_container_registry_tags
end
end
@@ -1937,7 +1940,7 @@ describe Project do
stub_container_registry_tags(repository: :any, tags: [])
end
- it 'should not have image tags' do
+ it 'does not have image tags' do
expect(project).not_to have_container_registry_tags
end
end
@@ -1948,16 +1951,16 @@ describe Project do
stub_container_registry_config(enabled: false)
end
- it 'should not have image tags' do
+ it 'does not have image tags' do
expect(project).not_to have_container_registry_tags
end
- it 'should not check root repository tags' do
+ it 'does not check root repository tags' do
expect(project).not_to receive(:full_path)
expect(project).not_to have_container_registry_tags
end
- it 'should iterate through container repositories' do
+ it 'iterates through container repositories' do
expect(project).to receive(:container_repositories)
expect(project).not_to have_container_registry_tags
end
@@ -2149,6 +2152,15 @@ describe Project do
expect(project.add_import_job).to eq(import_jid)
end
+
+ context 'without repository' do
+ it 'schedules RepositoryImportWorker' do
+ project = create(:project, import_url: generate(:url))
+
+ expect(RepositoryImportWorker).to receive(:perform_async).with(project.id).and_return(import_jid)
+ expect(project.add_import_job).to eq(import_jid)
+ end
+ end
end
context 'not forked' do
@@ -2360,6 +2372,18 @@ describe Project do
end
end
+ describe '#daily_statistics_enabled?' do
+ it { is_expected.to be_daily_statistics_enabled }
+
+ context 'when :project_daily_statistics is disabled for the project' do
+ before do
+ stub_feature_flags(project_daily_statistics: { thing: subject, enabled: false })
+ end
+
+ it { is_expected.not_to be_daily_statistics_enabled }
+ end
+ end
+
describe '#change_head' do
let(:project) { create(:project, :repository) }
@@ -2375,6 +2399,12 @@ describe Project do
project.change_head(project.default_branch)
end
+ it 'updates commit count' do
+ expect(ProjectCacheWorker).to receive(:perform_async).with(project.id, [], [:commit_count])
+
+ project.change_head(project.default_branch)
+ end
+
it 'copies the gitattributes' do
expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch)
project.change_head(project.default_branch)
@@ -2511,6 +2541,16 @@ describe Project do
end
end
+ describe '#set_repository_writable!' do
+ it 'sets repository_read_only to false' do
+ project = create(:project, :read_only)
+
+ expect { project.set_repository_writable! }
+ .to change(project, :repository_read_only)
+ .from(true).to(false)
+ end
+ end
+
describe '#pushes_since_gc' do
let(:project) { create(:project) }
@@ -2584,7 +2624,7 @@ describe Project do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
it 'returns variables from this service' do
expect(project.deployment_variables).to include(
- { key: 'KUBE_TOKEN', value: project.deployment_platform.token, public: false }
+ { key: 'KUBE_TOKEN', value: project.deployment_platform.token, public: false, masked: true }
)
end
end
@@ -2607,9 +2647,9 @@ describe Project do
let!(:cluster) { kubernetes_namespace.cluster }
let(:project) { kubernetes_namespace.project }
- it 'should return token from kubernetes namespace' do
+ it 'returns token from kubernetes namespace' do
expect(project.deployment_variables).to include(
- { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false }
+ { key: 'KUBE_TOKEN', value: kubernetes_namespace.service_account_token, public: false, masked: true }
)
end
end
@@ -3130,62 +3170,123 @@ describe Project do
end
end
- describe '.with_feature_available_for_user' do
+ describe '.ids_with_milestone_available_for' do
let!(:user) { create(:user) }
- let!(:feature) { MergeRequest }
- let!(:project) { create(:project, :public, :merge_requests_enabled) }
+
+ it 'returns project ids with milestones available for user' do
+ project_1 = create(:project, :public, :merge_requests_disabled, :issues_disabled)
+ project_2 = create(:project, :public, :merge_requests_disabled)
+ project_3 = create(:project, :public, :issues_disabled)
+ project_4 = create(:project, :public)
+ project_4.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, merge_requests_access_level: ProjectFeature::PRIVATE )
+
+ project_ids = described_class.ids_with_milestone_available_for(user).pluck(:id)
+
+ expect(project_ids).to include(project_2.id, project_3.id)
+ expect(project_ids).not_to include(project_1.id, project_4.id)
+ end
+ end
+
+ describe '.with_feature_available_for_user' do
+ let(:user) { create(:user) }
+ let(:feature) { MergeRequest }
subject { described_class.with_feature_available_for_user(feature, user) }
- context 'when user has access to project' do
- subject { described_class.with_feature_available_for_user(feature, user) }
+ shared_examples 'feature disabled' do
+ let(:project) { create(:project, :public, :merge_requests_disabled) }
+
+ it 'does not return projects with the project feature disabled' do
+ is_expected.not_to include(project)
+ end
+ end
+
+ shared_examples 'feature public' do
+ let(:project) { create(:project, :public, :merge_requests_public) }
+
+ it 'returns projects with the project feature public' do
+ is_expected.to include(project)
+ end
+ end
+
+ shared_examples 'feature enabled' do
+ let(:project) { create(:project, :public, :merge_requests_enabled) }
+ it 'returns projects with the project feature enabled' do
+ is_expected.to include(project)
+ end
+ end
+
+ shared_examples 'feature access level is nil' do
+ let(:project) { create(:project, :public) }
+
+ it 'returns projects with the project feature access level nil' do
+ project.project_feature.update(merge_requests_access_level: nil)
+
+ is_expected.to include(project)
+ end
+ end
+
+ context 'with user' do
before do
project.add_guest(user)
end
- context 'when public project' do
- context 'when feature is public' do
- it 'returns project' do
- is_expected.to include(project)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
+
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
+
+ context 'when user does not has access to the feature' do
+ it 'does not return projects with the project feature private' do
+ is_expected.not_to include(project)
end
end
- context 'when feature is private' do
- let!(:project) { create(:project, :public, :merge_requests_private) }
-
- it 'returns project when user has access to the feature' do
- project.add_maintainer(user)
+ context 'when user has access to the feature' do
+ it 'returns projects with the project feature private' do
+ project.add_reporter(user)
is_expected.to include(project)
end
-
- it 'does not return project when user does not have the minimum access level required' do
- is_expected.not_to include(project)
- end
end
end
+ end
- context 'when private project' do
- let!(:project) { create(:project) }
+ context 'user is an admin' do
+ let(:user) { create(:user, :admin) }
- it 'returns project when user has access to the feature' do
- project.add_maintainer(user)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
- is_expected.to include(project)
- end
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
- it 'does not return project when user does not have the minimum access level required' do
- is_expected.not_to include(project)
+ it 'returns projects with the project feature private' do
+ is_expected.to include(project)
end
end
end
- context 'when user does not have access to project' do
- let!(:project) { create(:project) }
+ context 'without user' do
+ let(:user) { nil }
- it 'does not return project when user cant access project' do
- is_expected.not_to include(project)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
+
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
+
+ it 'does not return projects with the project feature private' do
+ is_expected.not_to include(project)
+ end
end
end
end
@@ -3407,28 +3508,42 @@ describe Project do
project.migrate_to_hashed_storage!
end
- it 'schedules ProjectMigrateHashedStorageWorker with delayed start when the project repo is in use' do
- Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: false)).increase
+ it 'schedules HashedStorage::ProjectMigrateWorker with delayed start when the project repo is in use' do
+ Gitlab::ReferenceCounter.new(Gitlab::GlRepository::PROJECT.identifier_for_subject(project)).increase
- expect(ProjectMigrateHashedStorageWorker).to receive(:perform_in)
+ expect(HashedStorage::ProjectMigrateWorker).to receive(:perform_in)
project.migrate_to_hashed_storage!
end
- it 'schedules ProjectMigrateHashedStorageWorker with delayed start when the wiki repo is in use' do
- Gitlab::ReferenceCounter.new(project.gl_repository(is_wiki: true)).increase
+ it 'schedules HashedStorage::ProjectMigrateWorker with delayed start when the wiki repo is in use' do
+ Gitlab::ReferenceCounter.new(Gitlab::GlRepository::WIKI.identifier_for_subject(project)).increase
- expect(ProjectMigrateHashedStorageWorker).to receive(:perform_in)
+ expect(HashedStorage::ProjectMigrateWorker).to receive(:perform_in)
project.migrate_to_hashed_storage!
end
- it 'schedules ProjectMigrateHashedStorageWorker' do
- expect(ProjectMigrateHashedStorageWorker).to receive(:perform_async).with(project.id)
+ it 'schedules HashedStorage::ProjectMigrateWorker' do
+ expect(HashedStorage::ProjectMigrateWorker).to receive(:perform_async).with(project.id)
project.migrate_to_hashed_storage!
end
end
+
+ describe '#rollback_to_legacy_storage!' do
+ let(:project) { create(:project, :empty_repo, :legacy_storage) }
+
+ it 'returns nil' do
+ expect(project.rollback_to_legacy_storage!).to be_nil
+ end
+
+ it 'does not run validations' do
+ expect(project).not_to receive(:valid?)
+
+ project.rollback_to_legacy_storage!
+ end
+ end
end
context 'hashed storage' do
@@ -3504,20 +3619,34 @@ describe Project do
project = create(:project, storage_version: 1, skip_disk_validation: true)
Sidekiq::Testing.fake! do
- expect { project.migrate_to_hashed_storage! }.to change(ProjectMigrateHashedStorageWorker.jobs, :size).by(1)
+ expect { project.migrate_to_hashed_storage! }.to change(HashedStorage::ProjectMigrateWorker.jobs, :size).by(1)
end
end
end
end
- end
- describe '#gl_repository' do
- let(:project) { create(:project) }
+ describe '#rollback_to_legacy_storage!' do
+ let(:project) { create(:project, :repository, skip_disk_validation: true) }
+
+ it 'returns true' do
+ expect(project.rollback_to_legacy_storage!).to be_truthy
+ end
+
+ it 'does not run validations' do
+ expect(project).not_to receive(:valid?)
+
+ project.rollback_to_legacy_storage!
+ end
- it 'delegates to Gitlab::GlRepository.gl_repository' do
- expect(Gitlab::GlRepository).to receive(:gl_repository).with(project, true)
+ it 'does not flag as read-only' do
+ expect { project.rollback_to_legacy_storage! }.not_to change { project.repository_read_only }
+ end
- project.gl_repository(is_wiki: true)
+ it 'enqueues a job' do
+ Sidekiq::Testing.fake! do
+ expect { project.rollback_to_legacy_storage! }.to change(HashedStorage::ProjectRollbackWorker.jobs, :size).by(1)
+ end
+ end
end
end
@@ -3570,12 +3699,36 @@ describe Project do
subject { project.auto_devops_enabled? }
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when explicitly disabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
context 'when enabled in settings' do
before do
stub_application_setting(auto_devops_enabled: true)
end
it { is_expected.to be_truthy }
+ end
+
+ context 'when disabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it { is_expected.to be_falsey }
context 'when explicitly enabled' do
before do
@@ -3587,34 +3740,91 @@ describe Project do
context 'when explicitly disabled' do
before do
- create(:project_auto_devops, project: project, enabled: false)
+ create(:project_auto_devops, :disabled, project: project)
end
it { is_expected.to be_falsey }
end
end
- context 'when disabled in settings' do
+ context 'when force_autodevops_on_by_default is enabled for the project' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with group parents' do
+ let(:instance_enabled) { true }
+
before do
- stub_application_setting(auto_devops_enabled: false)
+ stub_application_setting(auto_devops_enabled: instance_enabled)
+ project.update!(namespace: parent_group)
end
- it { is_expected.to be_falsey }
+ context 'when enabled on parent' do
+ let(:parent_group) { create(:group, :auto_devops_enabled) }
- context 'when explicitly enabled' do
- before do
- create(:project_auto_devops, project: project)
+ context 'when auto devops instance enabled' do
+ it { is_expected.to be_truthy }
end
- it { is_expected.to be_truthy }
+ context 'when auto devops instance disabled' do
+ let(:instance_disabled) { false }
+
+ it { is_expected.to be_truthy }
+ end
end
- context 'when force_autodevops_on_by_default is enabled for the project' do
- before do
- Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100)
+ context 'when disabled on parent' do
+ let(:parent_group) { create(:group, :auto_devops_disabled) }
+
+ context 'when auto devops instance enabled' do
+ it { is_expected.to be_falsy }
end
- it { is_expected.to be_truthy }
+ context 'when auto devops instance disabled' do
+ let(:instance_disabled) { false }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when enabled on root parent', :nested_groups do
+ let(:parent_group) { create(:group, parent: create(:group, :auto_devops_enabled)) }
+
+ context 'when auto devops instance enabled' do
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when auto devops instance disabled' do
+ let(:instance_disabled) { false }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when explicitly disabled on parent' do
+ let(:parent_group) { create(:group, :auto_devops_disabled, parent: create(:group, :auto_devops_enabled)) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
+ context 'when disabled on root parent', :nested_groups do
+ let(:parent_group) { create(:group, parent: create(:group, :auto_devops_disabled)) }
+
+ context 'when auto devops instance enabled' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when auto devops instance disabled' do
+ let(:instance_disabled) { false }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'when explicitly disabled on parent' do
+ let(:parent_group) { create(:group, :auto_devops_disabled, parent: create(:group, :auto_devops_enabled)) }
+
+ it { is_expected.to be_falsy }
+ end
end
end
end
@@ -3661,15 +3871,52 @@ describe Project do
end
end
end
+
+ context 'when enabled on group' do
+ it 'has auto devops implicitly enabled' do
+ project.update(namespace: create(:group, :auto_devops_enabled))
+
+ expect(project).to have_auto_devops_implicitly_enabled
+ end
+ end
+
+ context 'when enabled on parent group' do
+ it 'has auto devops implicitly enabled' do
+ subgroup = create(:group, parent: create(:group, :auto_devops_enabled))
+ project.update(namespace: subgroup)
+
+ expect(project).to have_auto_devops_implicitly_enabled
+ end
+ end
end
describe '#has_auto_devops_implicitly_disabled?' do
+ set(:project) { create(:project) }
+
before do
allow(Feature).to receive(:enabled?).and_call_original
Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0)
end
- set(:project) { create(:project) }
+ context 'when explicitly disabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: false)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: true)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
context 'when enabled in settings' do
before do
@@ -3692,6 +3939,8 @@ describe Project do
context 'when force_autodevops_on_by_default is enabled for the project' do
before do
+ create(:project_auto_devops, project: project, enabled: false)
+
Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100)
end
@@ -3700,23 +3949,20 @@ describe Project do
end
end
- context 'when explicitly disabled' do
- before do
- create(:project_auto_devops, project: project, enabled: false)
- end
+ context 'when disabled on group' do
+ it 'has auto devops implicitly disabled' do
+ project.update!(namespace: create(:group, :auto_devops_disabled))
- it 'does not have auto devops implicitly disabled' do
- expect(project).not_to have_auto_devops_implicitly_disabled
+ expect(project).to have_auto_devops_implicitly_disabled
end
end
- context 'when explicitly enabled' do
- before do
- create(:project_auto_devops, project: project, enabled: true)
- end
+ context 'when disabled on parent group' do
+ it 'has auto devops implicitly disabled' do
+ subgroup = create(:group, parent: create(:group, :auto_devops_disabled))
+ project.update!(namespace: subgroup)
- it 'does not have auto devops implicitly disabled' do
- expect(project).not_to have_auto_devops_implicitly_disabled
+ expect(project).to have_auto_devops_implicitly_disabled
end
end
end
@@ -3746,64 +3992,6 @@ describe Project do
end
end
- describe '#auto_devops_variables' do
- set(:project) { create(:project) }
-
- subject { project.auto_devops_variables }
-
- context 'when enabled in instance settings' do
- before do
- stub_application_setting(auto_devops_enabled: true)
- end
-
- context 'when domain is empty' do
- before do
- stub_application_setting(auto_devops_domain: nil)
- end
-
- it 'variables does not include AUTO_DEVOPS_DOMAIN' do
- is_expected.not_to include(domain_variable)
- end
- end
-
- context 'when domain is configured' do
- before do
- stub_application_setting(auto_devops_domain: 'example.com')
- end
-
- it 'variables includes AUTO_DEVOPS_DOMAIN' do
- is_expected.to include(domain_variable)
- end
- end
- end
-
- context 'when explicitly enabled' do
- context 'when domain is empty' do
- before do
- create(:project_auto_devops, project: project, domain: nil)
- end
-
- it 'variables does not include AUTO_DEVOPS_DOMAIN' do
- is_expected.not_to include(domain_variable)
- end
- end
-
- context 'when domain is configured' do
- before do
- create(:project_auto_devops, project: project, domain: 'example.com')
- end
-
- it 'variables includes AUTO_DEVOPS_DOMAIN' do
- is_expected.to include(domain_variable)
- end
- end
- end
-
- def domain_variable
- { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true }
- end
- end
-
describe '#latest_successful_builds_for' do
let(:project) { build(:project) }
@@ -4194,6 +4382,25 @@ describe Project do
end
end
+ describe '#external_authorization_classification_label' do
+ it 'falls back to the default when none is configured' do
+ enable_external_authorization_service_check
+
+ expect(build(:project).external_authorization_classification_label)
+ .to eq('default_label')
+ end
+
+ it 'returns the classification label if it was configured on the project' do
+ enable_external_authorization_service_check
+
+ project = build(:project,
+ external_authorization_classification_label: 'hello')
+
+ expect(project.external_authorization_classification_label)
+ .to eq('hello')
+ end
+ end
+
describe "#pages_https_only?" do
subject { build(:project) }
@@ -4505,6 +4712,8 @@ describe Project do
it 'returns that pool repository' do
expect(subject).not_to be_empty
expect(subject[:pool_repository]).to be_persisted
+
+ expect(project.reload.pool_repository).to eq(subject[:pool_repository])
end
end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index 64c39f09e33..358873f9a2f 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe ProjectStatistics do
@@ -9,21 +11,37 @@ describe ProjectStatistics do
it { is_expected.to belong_to(:namespace) }
end
+ describe 'scopes' do
+ describe '.for_project_ids' do
+ it 'returns only requested projects' do
+ stats = create_list(:project_statistics, 3)
+ project_ids = stats[0..1].map { |s| s.project_id }
+ expected_ids = stats[0..1].map { |s| s.id }
+
+ requested_stats = described_class.for_project_ids(project_ids).pluck(:id)
+
+ expect(requested_stats).to eq(expected_ids)
+ end
+ end
+ end
+
describe 'statistics columns' do
it "support values up to 8 exabytes" do
statistics.update!(
commit_count: 8.exabytes - 1,
repository_size: 2.exabytes,
+ wiki_size: 1.exabytes,
lfs_objects_size: 2.exabytes,
- build_artifacts_size: 4.exabytes - 1
+ build_artifacts_size: 3.exabytes - 1
)
statistics.reload
expect(statistics.commit_count).to eq(8.exabytes - 1)
expect(statistics.repository_size).to eq(2.exabytes)
+ expect(statistics.wiki_size).to eq(1.exabytes)
expect(statistics.lfs_objects_size).to eq(2.exabytes)
- expect(statistics.build_artifacts_size).to eq(4.exabytes - 1)
+ expect(statistics.build_artifacts_size).to eq(3.exabytes - 1)
expect(statistics.storage_size).to eq(8.exabytes - 1)
end
end
@@ -31,6 +49,7 @@ describe ProjectStatistics do
describe '#total_repository_size' do
it "sums repository and LFS object size" do
statistics.repository_size = 2
+ statistics.wiki_size = 6
statistics.lfs_objects_size = 3
statistics.build_artifacts_size = 4
@@ -38,10 +57,17 @@ describe ProjectStatistics do
end
end
+ describe '#wiki_size' do
+ it "is initialized with not null value" do
+ expect(statistics.wiki_size).to eq 0
+ end
+ end
+
describe '#refresh!' do
before do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
+ allow(statistics).to receive(:update_wiki_size)
allow(statistics).to receive(:update_lfs_objects_size)
allow(statistics).to receive(:update_storage_size)
end
@@ -54,6 +80,7 @@ describe ProjectStatistics do
it "sums all counters" do
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
expect(statistics).to have_received(:update_lfs_objects_size)
end
end
@@ -67,6 +94,45 @@ describe ProjectStatistics do
expect(statistics).to have_received(:update_lfs_objects_size)
expect(statistics).not_to have_received(:update_commit_count)
expect(statistics).not_to have_received(:update_repository_size)
+ expect(statistics).not_to have_received(:update_wiki_size)
+ end
+ end
+
+ context 'without repositories' do
+ it 'does not crash' do
+ expect(project.repository.exists?).to be_falsey
+ expect(project.wiki.repository.exists?).to be_falsey
+
+ statistics.refresh!
+
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
+ expect(statistics.repository_size).to eq(0)
+ expect(statistics.commit_count).to eq(0)
+ expect(statistics.wiki_size).to eq(0)
+ end
+ end
+
+ context 'with deleted repositories' do
+ let(:project) { create(:project, :repository, :wiki_repo) }
+
+ before do
+ Gitlab::GitalyClient::StorageSettings.allow_disk_access do
+ FileUtils.rm_rf(project.repository.path)
+ FileUtils.rm_rf(project.wiki.repository.path)
+ end
+ end
+
+ it 'does not crash' do
+ statistics.refresh!
+
+ expect(statistics).to have_received(:update_commit_count)
+ expect(statistics).to have_received(:update_repository_size)
+ expect(statistics).to have_received(:update_wiki_size)
+ expect(statistics.repository_size).to eq(0)
+ expect(statistics.commit_count).to eq(0)
+ expect(statistics.wiki_size).to eq(0)
end
end
end
@@ -93,6 +159,17 @@ describe ProjectStatistics do
end
end
+ describe '#update_wiki_size' do
+ before do
+ allow(project.wiki.repository).to receive(:size).and_return(34)
+ statistics.update_wiki_size
+ end
+
+ it "stores the size of the wiki" do
+ expect(statistics.wiki_size).to eq 34.megabytes
+ end
+ end
+
describe '#update_lfs_objects_size' do
let!(:lfs_object1) { create(:lfs_object, size: 23.megabytes) }
let!(:lfs_object2) { create(:lfs_object, size: 34.megabytes) }
@@ -112,26 +189,41 @@ describe ProjectStatistics do
it "sums all storage counters" do
statistics.update!(
repository_size: 2,
+ wiki_size: 4,
lfs_objects_size: 3
)
statistics.reload
- expect(statistics.storage_size).to eq 5
+ expect(statistics.storage_size).to eq 9
end
end
describe '.increment_statistic' do
- it 'increases the statistic by that amount' do
- expect { described_class.increment_statistic(project.id, :build_artifacts_size, 13) }
- .to change { statistics.reload.build_artifacts_size }
- .by(13)
+ shared_examples 'a statistic that increases storage_size' do
+ it 'increases the statistic by that amount' do
+ expect { described_class.increment_statistic(project.id, stat, 13) }
+ .to change { statistics.reload.send(stat) || 0 }
+ .by(13)
+ end
+
+ it 'increases also storage size by that amount' do
+ expect { described_class.increment_statistic(project.id, stat, 20) }
+ .to change { statistics.reload.storage_size }
+ .by(20)
+ end
end
- it 'increases also storage size by that amount' do
- expect { described_class.increment_statistic(project.id, :build_artifacts_size, 20) }
- .to change { statistics.reload.storage_size }
- .by(20)
+ context 'when adjusting :build_artifacts_size' do
+ let(:stat) { :build_artifacts_size }
+
+ it_behaves_like 'a statistic that increases storage_size'
+ end
+
+ context 'when adjusting :packages_size' do
+ let(:stat) { :packages_size }
+
+ it_behaves_like 'a statistic that increases storage_size'
end
context 'when the amount is 0' do
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 3537dead5d1..77c88a04cde 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe ProjectTeam do
@@ -193,6 +195,30 @@ describe ProjectTeam do
end
end
+ describe '#add_users' do
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:project) { create(:project) }
+
+ it 'add the given users to the team' do
+ project.team.add_users([user1, user2], :reporter)
+
+ expect(project.team.reporter?(user1)).to be(true)
+ expect(project.team.reporter?(user2)).to be(true)
+ end
+ end
+
+ describe '#add_user' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ it 'add the given user to the team' do
+ project.team.add_user(user, :reporter)
+
+ expect(project.team.reporter?(user)).to be(true)
+ end
+ end
+
describe "#human_max_access" do
it 'returns Maintainer role' do
user = create(:user)
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 3ccc706edf2..d12dd97bb9e 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -1,4 +1,5 @@
-# coding: utf-8
+# frozen_string_literal: true
+
require "spec_helper"
describe ProjectWiki do
@@ -71,6 +72,14 @@ describe ProjectWiki do
expect(project_wiki.create_page("index", "test content")).to be_truthy
end
+ it "creates a new wiki repo with a default commit message" do
+ expect(project_wiki.create_page("index", "test content", :markdown, "")).to be_truthy
+
+ page = project_wiki.find_page('index')
+
+ expect(page.last_version.message).to eq("#{user.username} created page: index")
+ end
+
it "raises CouldNotCreateWikiError if it can't create the wiki repository" do
# Create a fresh project which will not have a wiki
project_wiki = described_class.new(create(:project), user)
@@ -100,8 +109,7 @@ describe ProjectWiki do
subject { super().empty? }
it { is_expected.to be_falsey }
- # Re-enable this when https://gitlab.com/gitlab-org/gitaly/issues/1204 is fixed
- xit 'only instantiates a Wiki page once' do
+ it 'only instantiates a Wiki page once' do
expect(WikiPage).to receive(:new).once.and_call_original
subject
@@ -110,22 +118,65 @@ describe ProjectWiki do
end
end
- describe "#pages" do
+ describe "#list_pages" do
+ let(:wiki_pages) { subject.list_pages }
+
before do
- create_page("index", "This is an awesome new Gollum Wiki")
- @pages = subject.pages
+ create_page("index", "This is an index")
+ create_page("index2", "This is an index2")
+ create_page("an index3", "This is an index3")
end
after do
- destroy_page(@pages.first.page)
+ wiki_pages.each do |wiki_page|
+ destroy_page(wiki_page.page)
+ end
end
it "returns an array of WikiPage instances" do
- expect(@pages.first).to be_a WikiPage
+ expect(wiki_pages.first).to be_a WikiPage
+ end
+
+ it 'does not load WikiPage content by default' do
+ wiki_pages.each do |page|
+ expect(page.content).to be_empty
+ end
+ end
+
+ it 'returns all pages by default' do
+ expect(wiki_pages.count).to eq(3)
end
- it "returns the correct number of pages" do
- expect(@pages.count).to eq(1)
+ context "with limit option" do
+ it 'returns limited set of pages' do
+ expect(subject.list_pages(limit: 1).count).to eq(1)
+ end
+ end
+
+ context "with sorting options" do
+ it 'returns pages sorted by title by default' do
+ pages = ['an index3', 'index', 'index2']
+
+ expect(subject.list_pages.map(&:title)).to eq(pages)
+ expect(subject.list_pages(direction: "desc").map(&:title)).to eq(pages.reverse)
+ end
+
+ it 'returns pages sorted by created_at' do
+ pages = ['index', 'index2', 'an index3']
+
+ expect(subject.list_pages(sort: 'created_at').map(&:title)).to eq(pages)
+ expect(subject.list_pages(sort: 'created_at', direction: "desc").map(&:title)).to eq(pages.reverse)
+ end
+ end
+
+ context "with load_content option" do
+ let(:pages) { subject.list_pages(load_content: true) }
+
+ it 'loads WikiPage content' do
+ expect(pages.first.content).to eq("This is an index3")
+ expect(pages.second.content).to eq("This is an index")
+ expect(pages.third.content).to eq("This is an index2")
+ end
end
end
@@ -135,7 +186,7 @@ describe ProjectWiki do
end
after do
- subject.pages.each { |page| destroy_page(page.page) }
+ subject.list_pages.each { |page| destroy_page(page.page) }
end
it "returns the latest version of the page if it exists" do
@@ -186,7 +237,7 @@ describe ProjectWiki do
end
after do
- subject.pages.each { |page| destroy_page(page.page) }
+ subject.list_pages.each { |page| destroy_page(page.page) }
end
it 'finds the page defined as _sidebar' do
@@ -233,12 +284,12 @@ describe ProjectWiki do
describe "#create_page" do
after do
- destroy_page(subject.pages.first.page)
+ destroy_page(subject.list_pages.first.page)
end
it "creates a new wiki page" do
expect(subject.create_page("test page", "this is content")).not_to eq(false)
- expect(subject.pages.count).to eq(1)
+ expect(subject.list_pages.count).to eq(1)
end
it "returns false when a duplicate page exists" do
@@ -253,7 +304,7 @@ describe ProjectWiki do
it "sets the correct commit message" do
subject.create_page("test page", "some content", :markdown, "commit message")
- expect(subject.pages.first.page.version.message).to eq("commit message")
+ expect(subject.list_pages.first.page.version.message).to eq("commit message")
end
it 'sets the correct commit email' do
@@ -284,7 +335,7 @@ describe ProjectWiki do
format: :markdown,
message: "updated page"
)
- @page = subject.pages.first.page
+ @page = subject.list_pages(load_content: true).first.page
end
after do
@@ -328,7 +379,7 @@ describe ProjectWiki do
it "deletes the page" do
subject.delete_page(@page)
- expect(subject.pages.count).to eq(0)
+ expect(subject.list_pages.count).to eq(0)
end
it 'sets the correct commit email' do
diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb
index d4433a88a15..aca3df9fdde 100644
--- a/spec/models/protectable_dropdown_spec.rb
+++ b/spec/models/protectable_dropdown_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectableDropdown do
diff --git a/spec/models/protected_branch/merge_access_level_spec.rb b/spec/models/protected_branch/merge_access_level_spec.rb
index 612e4a0e332..39dd586b157 100644
--- a/spec/models/protected_branch/merge_access_level_spec.rb
+++ b/spec/models/protected_branch/merge_access_level_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedBranch::MergeAccessLevel do
diff --git a/spec/models/protected_branch/push_access_level_spec.rb b/spec/models/protected_branch/push_access_level_spec.rb
index 9ccdc22fd41..628c8d29ecd 100644
--- a/spec/models/protected_branch/push_access_level_spec.rb
+++ b/spec/models/protected_branch/push_access_level_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedBranch::PushAccessLevel do
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 4c677200ae2..267434a4148 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedBranch do
@@ -190,4 +192,32 @@ describe ProtectedBranch do
end
end
end
+
+ describe '#any_protected?' do
+ context 'existing project' do
+ let(:project) { create(:project, :repository) }
+
+ it 'returns true when any of the branch names match a protected branch via direct match' do
+ create(:protected_branch, project: project, name: 'foo')
+
+ expect(described_class.any_protected?(project, ['foo', 'production/some-branch'])).to eq(true)
+ end
+
+ it 'returns true when any of the branch matches a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: 'production/*')
+
+ expect(described_class.any_protected?(project, ['foo', 'production/some-branch'])).to eq(true)
+ end
+
+ it 'returns false when none of branches does not match a protected branch via direct match' do
+ expect(described_class.any_protected?(project, ['foo'])).to eq(false)
+ end
+
+ it 'returns false when none of the branches does not match a protected branch via wildcard match' do
+ create(:protected_branch, project: project, name: 'production/*')
+
+ expect(described_class.any_protected?(project, ['staging/some-branch'])).to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/models/protected_tag_spec.rb b/spec/models/protected_tag_spec.rb
index e5a0f6ec23f..79120d17d39 100644
--- a/spec/models/protected_tag_spec.rb
+++ b/spec/models/protected_tag_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedTag do
diff --git a/spec/models/push_event_payload_spec.rb b/spec/models/push_event_payload_spec.rb
index 69a4922b6fd..6b59ee5ee57 100644
--- a/spec/models/push_event_payload_spec.rb
+++ b/spec/models/push_event_payload_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PushEventPayload do
diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb
index bfe7a30b96a..5509ed87308 100644
--- a/spec/models/push_event_spec.rb
+++ b/spec/models/push_event_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PushEvent do
@@ -121,9 +123,9 @@ describe PushEvent do
end
end
- describe '#push?' do
+ describe '#push_action?' do
it 'returns true' do
- expect(event).to be_push
+ expect(event).to be_push_action
end
end
diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb
index 106ae59af29..6ecb5c31c7e 100644
--- a/spec/models/redirect_route_spec.rb
+++ b/spec/models/redirect_route_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe RedirectRoute do
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 157c96c1f65..7c106ce6b85 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe Release do
@@ -16,6 +18,22 @@ RSpec.describe Release do
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_presence_of(:name) }
+
+ 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.description = "change"
+ existing_release_without_name.save
+ existing_release_without_name.reload
+
+ expect(existing_release_without_name).to be_valid
+ expect(existing_release_without_name.description).to eq("change")
+ expect(existing_release_without_name.name).to be_nil
+ end
+ end
end
describe '#assets_count' do
@@ -31,6 +49,11 @@ RSpec.describe Release do
it 'counts the link as an asset' do
is_expected.to eq(1 + Releases::Source::FORMATS.count)
end
+
+ it "excludes sources count when asked" do
+ assets_count = release.assets_count(except: [:sources])
+ expect(assets_count).to eq(1)
+ end
end
end
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index c06e9a08ab4..e14b19db915 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe RemoteMirror, :mailer do
@@ -5,14 +7,14 @@ describe RemoteMirror, :mailer do
describe 'URL validation' do
context 'with a valid URL' do
- it 'should be valid' do
+ it 'is valid' do
remote_mirror = build(:remote_mirror)
expect(remote_mirror).to be_valid
end
end
context 'with an invalid URL' do
- it 'should not be valid' do
+ it 'is not valid' do
remote_mirror = build(:remote_mirror, url: 'ftp://invalid.invalid')
expect(remote_mirror).not_to be_valid
@@ -371,6 +373,22 @@ describe RemoteMirror, :mailer do
end
end
+ describe '#disabled?' do
+ subject { remote_mirror.disabled? }
+
+ context 'when disabled' do
+ let(:remote_mirror) { build(:remote_mirror, enabled: false) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when enabled' do
+ let(:remote_mirror) { build(:remote_mirror, enabled: true) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
def create_mirror(params)
project = FactoryBot.create(:project, :repository)
project.remote_mirrors.create!(params)
diff --git a/spec/models/repository_language_spec.rb b/spec/models/repository_language_spec.rb
index e2e4beb512f..13a4cd1e7cf 100644
--- a/spec/models/repository_language_spec.rb
+++ b/spec/models/repository_language_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryLanguage do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f78760bf567..c5ab7e57272 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Repository do
@@ -48,7 +50,7 @@ describe Repository do
it { is_expected.not_to include('fix') }
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.branch_names_contains(sample_commit.id)
end
@@ -215,6 +217,25 @@ describe Repository do
expect(result.size).to eq(0)
end
+
+ context 'with a commit with invalid UTF-8 path' do
+ def create_commit_with_invalid_utf8_path
+ rugged = rugged_repo(repository)
+ blob_id = Rugged::Blob.from_buffer(rugged, "some contents")
+ tree_builder = Rugged::Tree::Builder.new(rugged)
+ tree_builder.insert({ oid: blob_id, name: "hello\x80world", filemode: 0100644 })
+ tree_id = tree_builder.write
+ user = { email: "jcai@gitlab.com", time: Time.now, name: "John Cai" }
+
+ Rugged::Commit.create(rugged, message: 'some commit message', parents: [rugged.head.target.oid], tree: tree_id, committer: user, author: user)
+ end
+
+ it 'does not raise an error' do
+ commit = create_commit_with_invalid_utf8_path
+
+ expect { repository.list_last_commits_for_tree(commit, '.', offset: 0) }.not_to raise_error
+ end
+ end
end
describe '#last_commit_for_path' do
@@ -223,7 +244,7 @@ describe Repository do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore')
end
@@ -247,7 +268,7 @@ describe Repository do
end
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id
end
@@ -388,7 +409,7 @@ describe Repository do
end
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') }
end
end
@@ -724,7 +745,7 @@ describe Repository do
end
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.search_files_by_content('feature', 'master')
end
@@ -773,7 +794,7 @@ describe Repository do
end
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') }
end
end
@@ -815,7 +836,7 @@ describe Repository do
let(:broken_repository) { create(:project, :broken_storage).repository }
describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
+ it 'raises a storage error' do
expect_to_raise_storage_error do
broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2')
end
@@ -1016,7 +1037,7 @@ describe Repository do
repository.add_branch(project.creator, ref, 'master')
end
- it 'should be true' do
+ it 'is true' do
is_expected.to eq(true)
end
end
@@ -1026,7 +1047,7 @@ describe Repository do
repository.add_tag(project.creator, ref, 'master')
end
- it 'should be false' do
+ it 'is false' do
is_expected.to eq(false)
end
end
@@ -1095,65 +1116,69 @@ describe Repository do
end
end
- describe '#exists?' do
- it 'returns true when a repository exists' do
- expect(repository.exists?).to be(true)
- end
-
- it 'returns false if no full path can be constructed' do
- allow(repository).to receive(:full_path).and_return(nil)
-
- expect(repository.exists?).to be(false)
- end
-
- context 'with broken storage', :broken_storage do
- it 'should raise a storage error' do
- expect_to_raise_storage_error { broken_repository.exists? }
- end
- end
-
+ shared_examples 'asymmetric cached method' do |method|
context 'asymmetric caching', :use_clean_rails_memory_store_caching, :request_store do
let(:cache) { repository.send(:cache) }
let(:request_store_cache) { repository.send(:request_store_cache) }
context 'when it returns true' do
before do
- expect(repository.raw_repository).to receive(:exists?).once.and_return(true)
+ expect(repository.raw_repository).to receive(method).once.and_return(true)
end
it 'caches the output in RequestStore' do
expect do
- repository.exists?
- end.to change { request_store_cache.read(:exists?) }.from(nil).to(true)
+ repository.send(method)
+ end.to change { request_store_cache.read(method) }.from(nil).to(true)
end
it 'caches the output in RepositoryCache' do
expect do
- repository.exists?
- end.to change { cache.read(:exists?) }.from(nil).to(true)
+ repository.send(method)
+ end.to change { cache.read(method) }.from(nil).to(true)
end
end
context 'when it returns false' do
before do
- expect(repository.raw_repository).to receive(:exists?).once.and_return(false)
+ expect(repository.raw_repository).to receive(method).once.and_return(false)
end
it 'caches the output in RequestStore' do
expect do
- repository.exists?
- end.to change { request_store_cache.read(:exists?) }.from(nil).to(false)
+ repository.send(method)
+ end.to change { request_store_cache.read(method) }.from(nil).to(false)
end
it 'does NOT cache the output in RepositoryCache' do
expect do
- repository.exists?
- end.not_to change { cache.read(:exists?) }.from(nil)
+ repository.send(method)
+ end.not_to change { cache.read(method) }.from(nil)
end
end
end
end
+ describe '#exists?' do
+ it 'returns true when a repository exists' do
+ expect(repository.exists?).to be(true)
+ end
+
+ it 'returns false if no full path can be constructed' do
+ allow(repository).to receive(:full_path).and_return(nil)
+
+ expect(repository.exists?).to be(false)
+ end
+
+ context 'with broken storage', :broken_storage do
+ it 'raises a storage error' do
+ expect_to_raise_storage_error { broken_repository.exists? }
+ end
+ end
+
+ it_behaves_like 'asymmetric cached method', :exists?
+ end
+
describe '#has_visible_content?' do
before do
# If raw_repository.has_visible_content? gets called more than once then
@@ -1271,6 +1296,8 @@ describe Repository do
repository.root_ref
repository.root_ref
end
+
+ it_behaves_like 'asymmetric cached method', :root_ref
end
describe '#expire_root_ref_cache' do
@@ -1373,6 +1400,29 @@ describe Repository do
end
end
+ describe '#merge_to_ref' do
+ let(:merge_request) do
+ create(:merge_request, source_branch: 'feature',
+ target_branch: 'master',
+ source_project: project)
+ end
+
+ it 'writes merge of source and target to MR merge_ref_path' do
+ merge_commit_id = repository.merge_to_ref(user,
+ merge_request.diff_head_sha,
+ merge_request,
+ merge_request.merge_ref_path,
+ 'Custom message')
+
+ merge_commit = repository.commit(merge_commit_id)
+
+ expect(merge_commit.message).to eq('Custom message')
+ expect(merge_commit.author_name).to eq(user.name)
+ expect(merge_commit.author_email).to eq(user.commit_email)
+ expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
+ end
+ end
+
describe '#ff_merge' do
before do
repository.add_branch(user, 'ff-target', 'feature~5')
@@ -1401,6 +1451,91 @@ describe Repository do
end
end
+ describe '#rebase' do
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project) }
+
+ shared_examples_for 'a method that can rebase successfully' do
+ it 'returns the rebase commit sha' do
+ rebase_commit_sha = repository.rebase(user, merge_request)
+ head_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
+
+ expect(rebase_commit_sha).to eq(head_sha)
+ end
+
+ it 'sets the `rebase_commit_sha` for the given merge request' do
+ rebase_commit_sha = repository.rebase(user, merge_request)
+
+ expect(rebase_commit_sha).not_to be_nil
+ expect(merge_request.rebase_commit_sha).to eq(rebase_commit_sha)
+ end
+ end
+
+ context 'when two_step_rebase feature is enabled' do
+ before do
+ stub_feature_flags(two_step_rebase: true)
+ end
+
+ it_behaves_like 'a method that can rebase successfully'
+
+ it 'executes the new Gitaly RPC' do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rebase)
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:user_rebase)
+
+ repository.rebase(user, merge_request)
+ end
+
+ describe 'rolling back the `rebase_commit_sha`' do
+ let(:new_sha) { Digest::SHA1.hexdigest('foo') }
+
+ it 'does not rollback when there are no errors' do
+ second_response = double(pre_receive_error: nil, git_error: nil)
+ mock_gitaly(second_response)
+
+ repository.rebase(user, merge_request)
+
+ expect(merge_request.reload.rebase_commit_sha).to eq(new_sha)
+ end
+
+ it 'does rollback when an error is encountered in the second step' do
+ second_response = double(pre_receive_error: 'my_error', git_error: nil)
+ mock_gitaly(second_response)
+
+ expect do
+ repository.rebase(user, merge_request)
+ end.to raise_error(Gitlab::Git::PreReceiveError)
+
+ expect(merge_request.reload.rebase_commit_sha).to be_nil
+ end
+
+ def mock_gitaly(second_response)
+ responses = [
+ double(rebase_sha: new_sha).as_null_object,
+ second_response
+ ]
+
+ expect_any_instance_of(
+ Gitaly::OperationService::Stub
+ ).to receive(:user_rebase_confirmable).and_return(responses.each)
+ end
+ end
+ end
+
+ context 'when two_step_rebase feature is disabled' do
+ before do
+ stub_feature_flags(two_step_rebase: false)
+ end
+
+ it_behaves_like 'a method that can rebase successfully'
+
+ it 'executes the deprecated Gitaly RPC' do
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:user_rebase)
+ expect_any_instance_of(Gitlab::GitalyClient::OperationService).not_to receive(:rebase)
+
+ repository.rebase(user, merge_request)
+ end
+ end
+ end
+
describe '#revert' do
let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') }
let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') }
@@ -1587,6 +1722,7 @@ describe Repository do
:has_visible_content?,
:issue_template_names,
:merge_request_template_names,
+ :metrics_dashboard_paths,
:xcode_project?
])
@@ -2150,12 +2286,45 @@ describe Repository do
end
describe '#diverging_commit_counts' do
+ let(:diverged_branch) { repository.find_branch('fix') }
+ let(:root_ref_sha) { repository.raw_repository.commit(repository.root_ref).id }
+ let(:diverged_branch_sha) { diverged_branch.dereferenced_target.sha }
+
it 'returns the commit counts behind and ahead of default branch' do
- result = repository.diverging_commit_counts(
- repository.find_branch('fix'))
+ result = repository.diverging_commit_counts(diverged_branch)
expect(result).to eq(behind: 29, ahead: 2)
end
+
+ context 'when gitaly_count_diverging_commits_no_max is enabled' do
+ before do
+ stub_feature_flags(gitaly_count_diverging_commits_no_max: true)
+ end
+
+ it 'calls diverging_commit_count without max count' do
+ expect(repository.raw_repository)
+ .to receive(:diverging_commit_count)
+ .with(root_ref_sha, diverged_branch_sha)
+ .and_return([29, 2])
+
+ repository.diverging_commit_counts(diverged_branch)
+ end
+ end
+
+ context 'when gitaly_count_diverging_commits_no_max is disabled' do
+ before do
+ stub_feature_flags(gitaly_count_diverging_commits_no_max: false)
+ end
+
+ it 'calls diverging_commit_count with max count' do
+ expect(repository.raw_repository)
+ .to receive(:diverging_commit_count)
+ .with(root_ref_sha, diverged_branch_sha, max_count: Repository::MAX_DIVERGING_COUNT)
+ .and_return([29, 2])
+
+ repository.diverging_commit_counts(diverged_branch)
+ end
+ end
end
describe '#refresh_method_caches' do
@@ -2214,15 +2383,15 @@ describe Repository do
rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target.id)
end
- describe '#ancestor?' do
+ shared_examples '#ancestor?' do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
- it 'it is an ancestor' do
+ it 'is an ancestor' do
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
end
- it 'it is not an ancestor' do
+ it 'is not an ancestor' do
expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
end
@@ -2238,6 +2407,20 @@ describe Repository do
end
end
+ describe '#ancestor? with Gitaly enabled' do
+ it_behaves_like "#ancestor?"
+ end
+
+ describe '#ancestor? with Rugged enabled', :enable_rugged do
+ it 'calls out to the Rugged implementation' do
+ allow_any_instance_of(Rugged).to receive(:merge_base).with(repository.commit.id, Gitlab::Git::BLANK_SHA).and_call_original
+
+ repository.ancestor?(repository.commit.id, Gitlab::Git::BLANK_SHA)
+ end
+
+ it_behaves_like '#ancestor?'
+ end
+
describe '#archive_metadata' do
let(:ref) { 'master' }
let(:storage_path) { '/tmp' }
@@ -2423,4 +2606,69 @@ describe Repository do
repository.merge_base('master', 'fix')
end
end
+
+ describe '#create_if_not_exists' do
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ it 'creates the repository if it did not exist' do
+ expect { repository.create_if_not_exists }.to change { repository.exists? }.from(false).to(true)
+ end
+
+ it 'calls out to the repository client to create a repo' do
+ expect(repository.raw.gitaly_repository_client).to receive(:create_repository)
+
+ repository.create_if_not_exists
+ end
+
+ context 'it does nothing if the repository already existed' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does nothing if the repository already existed' do
+ expect(repository.raw.gitaly_repository_client).not_to receive(:create_repository)
+
+ repository.create_if_not_exists
+ end
+ end
+
+ context 'when the repository exists but the cache is not up to date' do
+ let(:project) { create(:project, :repository) }
+
+ it 'does not raise errors' do
+ allow(repository).to receive(:exists?).and_return(false)
+ expect(repository.raw).to receive(:create_repository).and_call_original
+
+ expect { repository.create_if_not_exists }.not_to raise_error
+ end
+ end
+ end
+
+ describe "#blobs_metadata" do
+ set(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ def expect_metadata_blob(thing)
+ expect(thing).to be_a(Blob)
+ expect(thing.data).to be_empty
+ end
+
+ it "returns blob metadata in batch for HEAD" do
+ result = repository.blobs_metadata(["bar/branch-test.txt", "README.md", "does/not/exist"])
+
+ expect_metadata_blob(result.first)
+ expect_metadata_blob(result.second)
+ expect(result.size).to eq(2)
+ end
+
+ it "returns blob metadata for a specified ref" do
+ result = repository.blobs_metadata(["files/ruby/feature.rb"], "feature")
+
+ expect_metadata_blob(result.first)
+ end
+
+ it "performs a single gitaly call", :request_store do
+ expect { repository.blobs_metadata(["bar/branch-test.txt", "readme.txt", "does/not/exist"]) }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(1)
+ end
+ end
end
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index 7eeb2fae57d..cb52f154299 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -82,13 +82,13 @@ RSpec.describe ResourceLabelEvent, type: :model do
end
it 'returns true if markdown is outdated' do
- subject.attributes = { cached_markdown_version: ((CacheMarkdownField::CACHE_COMMONMARK_VERSION - 1) << 16) | 0 }
+ subject.attributes = { cached_markdown_version: ((Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION - 1) << 16) | 0 }
expect(subject.outdated_markdown?).to be true
end
it 'returns false if label and reference are set' do
- subject.attributes = { reference: 'whatever', cached_markdown_version: CacheMarkdownField::CACHE_COMMONMARK_VERSION << 16 }
+ subject.attributes = { reference: 'whatever', cached_markdown_version: Gitlab::MarkdownCache::CACHE_COMMONMARK_VERSION << 16 }
expect(subject.outdated_markdown?).to be false
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 48799781b87..20289afbeb5 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Route do
diff --git a/spec/models/sent_notification_spec.rb b/spec/models/sent_notification_spec.rb
index 6c35ed8f649..09be90b82ed 100644
--- a/spec/models/sent_notification_spec.rb
+++ b/spec/models/sent_notification_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SentNotification do
diff --git a/spec/models/serverless/function_spec.rb b/spec/models/serverless/function_spec.rb
new file mode 100644
index 00000000000..1854d5f9415
--- /dev/null
+++ b/spec/models/serverless/function_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Serverless::Function do
+ let(:project) { create(:project) }
+ let(:func) { described_class.new(project, 'test', 'test-ns') }
+
+ it 'has a proper id' do
+ expect(func.id).to eql("#{project.id}/test/test-ns")
+ expect(func.name).to eql("test")
+ expect(func.namespace).to eql("test-ns")
+ end
+
+ it 'can decode an identifier' do
+ f = described_class.find_by_id("#{project.id}/testfunc/dummy-ns")
+
+ expect(f.name).to eql("testfunc")
+ expect(f.namespace).to eql("dummy-ns")
+ end
+end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 25eecb3f909..64db32781fe 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Service do
@@ -289,7 +291,7 @@ describe Service do
describe "#deprecated?" do
let(:project) { create(:project, :repository) }
- it 'should return false by default' do
+ it 'returns false by default' do
service = create(:service, project: project)
expect(service.deprecated?).to be_falsy
end
@@ -298,7 +300,7 @@ describe Service do
describe "#deprecation_message" do
let(:project) { create(:project, :repository) }
- it 'should be empty by default' do
+ it 'is empty by default' do
service = create(:service, project: project)
expect(service.deprecation_message).to be_nil
end
diff --git a/spec/models/snippet_blob_spec.rb b/spec/models/snippet_blob_spec.rb
index 7c71c458fcc..88441e39d45 100644
--- a/spec/models/snippet_blob_spec.rb
+++ b/spec/models/snippet_blob_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SnippetBlob do
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 664dc3fa145..3524cdae3b8 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Snippet do
diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb
index 90a2caaeb88..e9ea234f75d 100644
--- a/spec/models/spam_log_spec.rb
+++ b/spec/models/spam_log_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SpamLog do
diff --git a/spec/models/ssh_host_key_spec.rb b/spec/models/ssh_host_key_spec.rb
index 4c677569561..a17cd8ba345 100644
--- a/spec/models/ssh_host_key_spec.rb
+++ b/spec/models/ssh_host_key_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SshHostKey do
diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb
index 9e4c2620d82..41bd48810b2 100644
--- a/spec/models/subscription_spec.rb
+++ b/spec/models/subscription_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Subscription do
diff --git a/spec/models/suggestion_spec.rb b/spec/models/suggestion_spec.rb
index cafc725dddb..8d4e9070b19 100644
--- a/spec/models/suggestion_spec.rb
+++ b/spec/models/suggestion_spec.rb
@@ -21,6 +21,22 @@ describe Suggestion do
end
end
+ describe '#diff_lines' do
+ let(:suggestion) { create(:suggestion, :content_from_repo) }
+
+ it 'returns parsed diff lines' do
+ expected_diff_lines = Gitlab::Diff::SuggestionDiff.new(suggestion).diff_lines
+ diff_lines = suggestion.diff_lines
+
+ expect(diff_lines.size).to eq(expected_diff_lines.size)
+ expect(diff_lines).to all(be_a(Gitlab::Diff::Line))
+
+ expected_diff_lines.each_with_index do |expected_line, index|
+ expect(diff_lines[index].to_hash).to eq(expected_line.to_hash)
+ end
+ end
+ end
+
describe '#appliable?' do
context 'when note does not support suggestions' do
it 'returns false' do
diff --git a/spec/models/system_note_metadata_spec.rb b/spec/models/system_note_metadata_spec.rb
index 1e3f587e460..bcd3c03f947 100644
--- a/spec/models/system_note_metadata_spec.rb
+++ b/spec/models/system_note_metadata_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SystemNoteMetadata do
diff --git a/spec/models/term_agreement_spec.rb b/spec/models/term_agreement_spec.rb
index 950dfa09a6a..42a48048b67 100644
--- a/spec/models/term_agreement_spec.rb
+++ b/spec/models/term_agreement_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TermAgreement do
diff --git a/spec/models/timelog_spec.rb b/spec/models/timelog_spec.rb
index a0c93c531ea..9d69a0ab148 100644
--- a/spec/models/timelog_spec.rb
+++ b/spec/models/timelog_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe Timelog do
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 3682e21ca40..b5bf294790a 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Todo do
diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb
index 6bdb62a0864..c2d5dfdf9c4 100644
--- a/spec/models/tree_spec.rb
+++ b/spec/models/tree_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Tree do
diff --git a/spec/models/trending_project_spec.rb b/spec/models/trending_project_spec.rb
index 3b5e7ca0d39..619fc8e7d38 100644
--- a/spec/models/trending_project_spec.rb
+++ b/spec/models/trending_project_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TrendingProject do
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 5a0df9fbbb0..02702cb2497 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Upload do
diff --git a/spec/models/user_agent_detail_spec.rb b/spec/models/user_agent_detail_spec.rb
index b4669f8c1c2..f191d245045 100644
--- a/spec/models/user_agent_detail_spec.rb
+++ b/spec/models/user_agent_detail_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe UserAgentDetail do
diff --git a/spec/models/user_callout_spec.rb b/spec/models/user_callout_spec.rb
index d54355afe12..b87f6f03d6f 100644
--- a/spec/models/user_callout_spec.rb
+++ b/spec/models/user_callout_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe UserCallout do
diff --git a/spec/models/user_custom_attribute_spec.rb b/spec/models/user_custom_attribute_spec.rb
index 37fc3cb64f0..d0981b2d771 100644
--- a/spec/models/user_custom_attribute_spec.rb
+++ b/spec/models/user_custom_attribute_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UserCustomAttribute do
diff --git a/spec/models/user_interacted_project_spec.rb b/spec/models/user_interacted_project_spec.rb
index cb4bb3372d4..47d919c1d12 100644
--- a/spec/models/user_interacted_project_spec.rb
+++ b/spec/models/user_interacted_project_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UserInteractedProject do
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index b2ef17a81d4..e09c91e874a 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -73,4 +73,10 @@ describe UserPreference do
it_behaves_like 'a sort_by preference'
end
end
+
+ describe '#timezone' do
+ it 'returns server time as default' do
+ expect(user_preference.timezone).to eq(Time.zone.tzinfo.name)
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 85b157a9435..d1338e34bb8 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe User do
@@ -96,6 +98,11 @@ describe User do
end
describe 'validations' do
+ describe 'name' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_length_of(:name).is_at_most(128) }
+ end
+
describe 'username' do
it 'validates presence' do
expect(subject).to validate_presence_of(:username)
@@ -660,6 +667,68 @@ describe User do
end
end
+ describe '#highest_role' do
+ let(:user) { create(:user) }
+
+ let(:group) { create(:group) }
+
+ it 'returns NO_ACCESS if none has been set' do
+ expect(user.highest_role).to eq(Gitlab::Access::NO_ACCESS)
+ end
+
+ it 'returns MAINTAINER if user is maintainer of a project' do
+ create(:project, group: group) do |project|
+ project.add_maintainer(user)
+ end
+
+ expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER)
+ end
+
+ it 'returns the highest role if user is member of multiple projects' do
+ create(:project, group: group) do |project|
+ project.add_maintainer(user)
+ end
+
+ create(:project, group: group) do |project|
+ project.add_developer(user)
+ end
+
+ expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER)
+ end
+
+ it 'returns MAINTAINER if user is maintainer of a group' do
+ create(:group) do |group|
+ group.add_user(user, GroupMember::MAINTAINER)
+ end
+
+ expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER)
+ end
+
+ it 'returns the highest role if user is member of multiple groups' do
+ create(:group) do |group|
+ group.add_user(user, GroupMember::MAINTAINER)
+ end
+
+ create(:group) do |group|
+ group.add_user(user, GroupMember::DEVELOPER)
+ end
+
+ expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER)
+ end
+
+ it 'returns the highest role if user is member of multiple groups and projects' do
+ create(:group) do |group|
+ group.add_user(user, GroupMember::DEVELOPER)
+ end
+
+ create(:project, group: group) do |project|
+ project.add_maintainer(user)
+ end
+
+ expect(user.highest_role).to eq(Gitlab::Access::MAINTAINER)
+ end
+ end
+
describe '#update_tracked_fields!', :clean_gitlab_redis_shared_state do
let(:request) { OpenStruct.new(remote_ip: "127.0.0.1") }
let(:user) { create(:user) }
@@ -2747,9 +2816,9 @@ describe User do
project = create(:project, :public)
archived_project = create(:project, :public, :archived)
- create(:merge_request, source_project: project, author: user, assignee: user)
- create(:merge_request, :closed, source_project: project, author: user, assignee: user)
- create(:merge_request, source_project: archived_project, author: user, assignee: user)
+ create(:merge_request, source_project: project, author: user, assignees: [user])
+ create(:merge_request, :closed, source_project: project, author: user, assignees: [user])
+ create(:merge_request, source_project: archived_project, author: user, assignees: [user])
expect(user.assigned_open_merge_requests_count(force: true)).to eq 1
end
diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
index fb8575cfe2b..5fbcccf897e 100644
--- a/spec/models/wiki_directory_spec.rb
+++ b/spec/models/wiki_directory_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
RSpec.describe WikiDirectory do
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index cba22b2cc4e..520a06e138e 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe WikiPage do
@@ -18,12 +20,18 @@ describe WikiPage do
context 'when there are pages' do
before do
create_page('dir_1/dir_1_1/page_3', 'content')
+ create_page('page_1', 'content')
create_page('dir_1/page_2', 'content')
+ create_page('dir_2', 'page with dir name')
create_page('dir_2/page_5', 'content')
+ create_page('page_6', 'content')
create_page('dir_2/page_4', 'content')
- create_page('page_1', 'content')
end
+
let(:page_1) { wiki.find_page('page_1') }
+ let(:page_6) { wiki.find_page('page_6') }
+ let(:page_dir_2) { wiki.find_page('dir_2') }
+
let(:dir_1) do
WikiDirectory.new('dir_1', [wiki.find_page('dir_1/page_2')])
end
@@ -36,34 +44,49 @@ describe WikiPage do
WikiDirectory.new('dir_2', pages)
end
- it 'returns an array with pages and directories' do
- expected_grouped_entries = [page_1, dir_1, dir_1_1, dir_2]
-
- grouped_entries = described_class.group_by_directory(wiki.pages)
+ context "#list_pages" do
+ context 'sort by title' do
+ let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages) }
+ let(:expected_grouped_entries) { [dir_1_1, dir_1, page_dir_2, dir_2, page_1, page_6] }
- grouped_entries.each_with_index do |page_or_dir, i|
- expected_page_or_dir = expected_grouped_entries[i]
- expected_slugs = get_slugs(expected_page_or_dir)
- slugs = get_slugs(page_or_dir)
+ it 'returns an array with pages and directories' do
+ grouped_entries.each_with_index do |page_or_dir, i|
+ expected_page_or_dir = expected_grouped_entries[i]
+ expected_slugs = get_slugs(expected_page_or_dir)
+ slugs = get_slugs(page_or_dir)
- expect(slugs).to match_array(expected_slugs)
+ expect(slugs).to match_array(expected_slugs)
+ end
+ end
end
- end
- it 'returns an array sorted by alphabetical position' do
- # Directories and pages within directories are sorted alphabetically.
- # Pages at root come before everything.
- expected_order = ['page_1', 'dir_1/page_2', 'dir_1/dir_1_1/page_3',
- 'dir_2/page_4', 'dir_2/page_5']
+ context 'sort by created_at' do
+ let(:grouped_entries) { described_class.group_by_directory(wiki.list_pages(sort: 'created_at')) }
+ let(:expected_grouped_entries) { [dir_1_1, page_1, dir_1, page_dir_2, dir_2, page_6] }
- grouped_entries = described_class.group_by_directory(wiki.pages)
+ it 'returns an array with pages and directories' do
+ grouped_entries.each_with_index do |page_or_dir, i|
+ expected_page_or_dir = expected_grouped_entries[i]
+ expected_slugs = get_slugs(expected_page_or_dir)
+ slugs = get_slugs(page_or_dir)
- actual_order =
- grouped_entries.map do |page_or_dir|
- get_slugs(page_or_dir)
+ expect(slugs).to match_array(expected_slugs)
+ end
end
- .flatten
- expect(actual_order).to eq(expected_order)
+ end
+
+ it 'returns an array with retained order with directories at the top' do
+ expected_order = ['dir_1/dir_1_1/page_3', 'dir_1/page_2', 'dir_2', 'dir_2/page_4', 'dir_2/page_5', 'page_1', 'page_6']
+
+ grouped_entries = described_class.group_by_directory(wiki.list_pages)
+
+ actual_order =
+ grouped_entries.map do |page_or_dir|
+ get_slugs(page_or_dir)
+ end
+ .flatten
+ expect(actual_order).to eq(expected_order)
+ end
end
end
end
@@ -365,7 +388,7 @@ describe WikiPage do
it "deletes the page" do
@page.delete
- expect(wiki.pages).to be_empty
+ expect(wiki.list_pages).to be_empty
end
it "returns true" do
diff --git a/spec/policies/base_policy_spec.rb b/spec/policies/base_policy_spec.rb
index c03d95b34db..09be831dcd5 100644
--- a/spec/policies/base_policy_spec.rb
+++ b/spec/policies/base_policy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe BasePolicy do
+ include ExternalAuthorizationServiceHelpers
+
describe '.class_for' do
it 'detects policy class based on the subject ancestors' do
expect(DeclarativePolicy.class_for(GenericCommitStatus.new)).to eq(CommitStatusPolicy)
@@ -16,4 +18,25 @@ describe BasePolicy do
expect(DeclarativePolicy.class_for(:global)).to eq(GlobalPolicy)
end
end
+
+ describe 'read cross project' do
+ let(:current_user) { create(:user) }
+ let(:user) { create(:user) }
+
+ subject { described_class.new(current_user, [user]) }
+
+ it { is_expected.to be_allowed(:read_cross_project) }
+
+ context 'when an external authorization service is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it { is_expected.not_to be_allowed(:read_cross_project) }
+
+ it 'allows admins' do
+ expect(described_class.new(build(:admin), nil)).to be_allowed(:read_cross_project)
+ end
+ end
+ end
end
diff --git a/spec/policies/board_policy_spec.rb b/spec/policies/board_policy_spec.rb
index 4b76d65ef69..52c23951e37 100644
--- a/spec/policies/board_policy_spec.rb
+++ b/spec/policies/board_policy_spec.rb
@@ -17,14 +17,6 @@ describe BoardPolicy do
]
end
- def expect_allowed(*permissions)
- permissions.each { |p| is_expected.to be_allowed(p) }
- end
-
- def expect_disallowed(*permissions)
- permissions.each { |p| is_expected.not_to be_allowed(p) }
- end
-
context 'group board' do
subject { described_class.new(user, group_board) }
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
index 844d96017de..126d44d1860 100644
--- a/spec/policies/ci/pipeline_policy_spec.rb
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -100,5 +100,51 @@ describe Ci::PipelinePolicy, :models do
end
end
end
+
+ describe 'read_pipeline_variable' do
+ let(:project) { create(:project, :public) }
+
+ context 'when user has owner access' do
+ let(:user) { project.owner }
+
+ it 'is enabled' do
+ expect(policy).to be_allowed :read_pipeline_variable
+ end
+ end
+
+ context 'when user is developer and the creator of the pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, user: user) }
+
+ before do
+ project.add_developer(user)
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'is enabled' do
+ expect(policy).to be_allowed :read_pipeline_variable
+ end
+ end
+
+ context 'when user is developer and it is not the creator of the pipeline' do
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, user: project.owner) }
+
+ before do
+ project.add_developer(user)
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'is disabled' do
+ expect(policy).to be_disallowed :read_pipeline_variable
+ end
+ end
+
+ context 'when user is not owner nor developer' do
+ it 'is disabled' do
+ expect(policy).not_to be_allowed :read_pipeline_variable
+ end
+ end
+ end
end
end
diff --git a/spec/policies/clusters/cluster_policy_spec.rb b/spec/policies/clusters/cluster_policy_spec.rb
index b2f0ca1bc30..cc3dde154dc 100644
--- a/spec/policies/clusters/cluster_policy_spec.rb
+++ b/spec/policies/clusters/cluster_policy_spec.rb
@@ -66,5 +66,21 @@ describe Clusters::ClusterPolicy, :models do
it { expect(policy).to be_disallowed :admin_cluster }
end
end
+
+ context 'instance cluster' do
+ let(:cluster) { create(:cluster, :instance) }
+
+ context 'when user' do
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when admin' do
+ let(:user) { create(:admin) }
+
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+ end
end
end
diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb
new file mode 100644
index 00000000000..9d755c6d29d
--- /dev/null
+++ b/spec/policies/clusters/instance_policy_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::InstancePolicy do
+ let(:user) { create(:user) }
+ let(:policy) { described_class.new(user, Clusters::Instance.new) }
+
+ describe 'rules' do
+ context 'when user' do
+ it { expect(policy).to be_disallowed :read_cluster }
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+
+ context 'when admin' do
+ let(:user) { create(:admin) }
+
+ context 'with instance_level_clusters enabled' do
+ it { expect(policy).to be_allowed :read_cluster }
+ it { expect(policy).to be_allowed :update_cluster }
+ it { expect(policy).to be_allowed :admin_cluster }
+ end
+
+ context 'with instance_level_clusters disabled' do
+ before do
+ stub_feature_flags(instance_clusters: false)
+ end
+
+ it { expect(policy).to be_disallowed :read_cluster }
+ it { expect(policy).to be_disallowed :update_cluster }
+ it { expect(policy).to be_disallowed :admin_cluster }
+ end
+ end
+ end
+end
diff --git a/spec/policies/commit_policy_spec.rb b/spec/policies/commit_policy_spec.rb
new file mode 100644
index 00000000000..41f6fb08426
--- /dev/null
+++ b/spec/policies/commit_policy_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CommitPolicy do
+ describe '#rules' do
+ let(:user) { create(:user) }
+ let(:commit) { project.repository.head_commit }
+ let(:policy) { described_class.new(user, commit) }
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public, :repository) }
+
+ it 'can read commit and create a note' do
+ expect(policy).to be_allowed(:read_commit)
+ end
+
+ context 'when repository access level is private' do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it 'can not read commit and create a note' do
+ expect(policy).to be_disallowed(:read_commit)
+ end
+
+ context 'when the user is a project member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'can read commit and create a note' do
+ expect(policy).to be_allowed(:read_commit)
+ end
+ end
+ end
+ end
+
+ context 'when project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ it 'can not read commit and create a note' do
+ expect(policy).to be_disallowed(:read_commit)
+ end
+
+ context 'when the user is a project member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'can read commit and create a note' do
+ expect(policy).to be_allowed(:read_commit)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index 30d68e7dc9d..12be3927e18 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -181,6 +181,18 @@ describe GlobalPolicy do
end
end
+ describe 'read instance metadata' do
+ context 'regular user' do
+ it { is_expected.to be_allowed(:read_instance_metadata) }
+ end
+
+ context 'anonymous' do
+ let(:current_user) { nil }
+
+ it { is_expected.not_to be_allowed(:read_instance_metadata) }
+ end
+ end
+
describe 'read instance statistics' do
context 'regular user' do
it { is_expected.to be_allowed(:read_instance_statistics) }
diff --git a/spec/policies/group_member_policy_spec.rb b/spec/policies/group_member_policy_spec.rb
new file mode 100644
index 00000000000..7bd7184cffe
--- /dev/null
+++ b/spec/policies/group_member_policy_spec.rb
@@ -0,0 +1,105 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GroupMemberPolicy do
+ let(:guest) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :private) }
+
+ before do
+ group.add_guest(guest)
+ group.add_owner(owner)
+ end
+
+ let(:member_related_permissions) do
+ [:update_group_member, :destroy_group_member]
+ end
+
+ let(:membership) { current_user.members.first }
+
+ subject { described_class.new(current_user, membership) }
+
+ def expect_allowed(*permissions)
+ permissions.each { |p| is_expected.to be_allowed(p) }
+ end
+
+ def expect_disallowed(*permissions)
+ permissions.each { |p| is_expected.not_to be_allowed(p) }
+ end
+
+ context 'with guest user' do
+ let(:current_user) { guest }
+
+ it do
+ expect_disallowed(:member_related_permissions)
+ end
+ end
+
+ context 'with one owner' do
+ let(:current_user) { owner }
+
+ it do
+ expect_disallowed(:destroy_group_member)
+ expect_disallowed(:update_group_member)
+ end
+ end
+
+ context 'with more than one owner' do
+ let(:current_user) { owner }
+
+ before do
+ group.add_owner(create(:user))
+ end
+
+ it do
+ expect_allowed(:destroy_group_member)
+ expect_allowed(:update_group_member)
+ end
+ end
+
+ context 'with the group parent', :postgresql do
+ let(:current_user) { create :user }
+ let(:subgroup) { create(:group, :private, parent: group)}
+
+ before do
+ group.add_owner(owner)
+ subgroup.add_owner(current_user)
+ end
+
+ it do
+ expect_allowed(:destroy_group_member)
+ expect_allowed(:update_group_member)
+ end
+ end
+
+ context 'without group parent' do
+ let(:current_user) { create :user }
+ let(:subgroup) { create(:group, :private)}
+
+ before do
+ subgroup.add_owner(current_user)
+ end
+
+ it do
+ expect_disallowed(:destroy_group_member)
+ expect_disallowed(:update_group_member)
+ end
+ end
+
+ context 'without group parent with two owners' do
+ let(:current_user) { create :user }
+ let(:other_user) { create :user }
+ let(:subgroup) { create(:group, :private)}
+
+ before do
+ subgroup.add_owner(current_user)
+ subgroup.add_owner(other_user)
+ end
+
+ it do
+ expect_allowed(:destroy_group_member)
+ expect_allowed(:update_group_member)
+ end
+ end
+end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index be1804c5ce0..59f3a961d50 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -1,63 +1,7 @@
require 'spec_helper'
describe GroupPolicy do
- let(:guest) { create(:user) }
- let(:reporter) { create(:user) }
- let(:developer) { create(:user) }
- let(:maintainer) { create(:user) }
- let(:owner) { create(:user) }
- let(:admin) { create(:admin) }
- let(:group) { create(:group, :private) }
-
- let(:guest_permissions) do
- [:read_label, :read_group, :upload_file, :read_namespace, :read_group_activity,
- :read_group_issues, :read_group_boards, :read_group_labels, :read_group_milestones,
- :read_group_merge_requests]
- end
-
- let(:reporter_permissions) { [:admin_label] }
-
- let(:developer_permissions) { [:admin_milestone] }
-
- let(:maintainer_permissions) do
- [
- :create_projects,
- :read_cluster,
- :create_cluster,
- :update_cluster,
- :admin_cluster,
- :add_cluster
- ]
- end
-
- let(:owner_permissions) do
- [
- :admin_group,
- :admin_namespace,
- :admin_group_member,
- :change_visibility_level,
- :set_note_created_at,
- (Gitlab::Database.postgresql? ? :create_subgroup : nil)
- ].compact
- end
-
- before do
- group.add_guest(guest)
- group.add_reporter(reporter)
- group.add_developer(developer)
- group.add_maintainer(maintainer)
- group.add_owner(owner)
- end
-
- subject { described_class.new(current_user, group) }
-
- def expect_allowed(*permissions)
- permissions.each { |p| is_expected.to be_allowed(p) }
- end
-
- def expect_disallowed(*permissions)
- permissions.each { |p| is_expected.not_to be_allowed(p) }
- end
+ include_context 'GroupPolicy context'
context 'with no user' do
let(:group) { create(:group, :public) }
@@ -74,6 +18,29 @@ describe GroupPolicy do
end
end
+ context 'with no user and public project' do
+ let(:project) { create(:project, :public) }
+ let(:current_user) { nil }
+
+ before do
+ create(:project_group_link, project: project, group: group)
+ end
+
+ it { expect_disallowed(:read_group) }
+ end
+
+ context 'with foreign user and public project' do
+ let(:project) { create(:project, :public) }
+ let(:user) { create(:user) }
+ let(:current_user) { create(:user) }
+
+ before do
+ create(:project_group_link, project: project, group: group)
+ end
+
+ it { expect_disallowed(:read_group) }
+ end
+
context 'has projects' do
let(:current_user) { create(:user) }
let(:project) { create(:project, namespace: group) }
@@ -82,17 +49,13 @@ describe GroupPolicy do
project.add_developer(current_user)
end
- it do
- expect_allowed(:read_group, :read_label)
- end
+ it { expect_allowed(:read_label, :read_list) }
context 'in subgroups', :nested_groups do
let(:subgroup) { create(:group, :private, parent: group) }
let(:project) { create(:project, namespace: subgroup) }
- it do
- expect_allowed(:read_group, :read_label)
- end
+ it { expect_allowed(:read_label, :read_list) }
end
end
@@ -384,6 +347,120 @@ describe GroupPolicy do
end
end
+ context "create_projects" do
+ context 'when group has no project creation level set' do
+ let(:group) { create(:group, project_creation_level: nil) }
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+ end
+
+ context 'when group has project creation level set to no one' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS) }
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+ end
+
+ context 'when group has project creation level set to maintainer only' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::MAINTAINER_PROJECT_ACCESS) }
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+ end
+
+ context 'when group has project creation level set to developers + maintainer' do
+ let(:group) { create(:group, project_creation_level: ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS) }
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_projects) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_projects) }
+ end
+ end
+ end
+
it_behaves_like 'clusterable policies' do
let(:clusterable) { create(:group) }
let(:cluster) do
diff --git a/spec/policies/identity_provider_policy_spec.rb b/spec/policies/identity_provider_policy_spec.rb
new file mode 100644
index 00000000000..2520469d4e7
--- /dev/null
+++ b/spec/policies/identity_provider_policy_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IdentityProviderPolicy do
+ subject(:policy) { described_class.new(user, provider) }
+ let(:user) { User.new }
+ let(:provider) { :a_provider }
+
+ describe '#rules' do
+ it { is_expected.to be_allowed(:link) }
+ it { is_expected.to be_allowed(:unlink) }
+
+ context 'when user is anonymous' do
+ let(:user) { nil }
+
+ it { is_expected.not_to be_allowed(:link) }
+ it { is_expected.not_to be_allowed(:unlink) }
+ end
+
+ %w[saml cas3].each do |provider_name|
+ context "when provider is #{provider_name}" do
+ let(:provider) { provider_name }
+
+ it { is_expected.to be_allowed(:link) }
+ it { is_expected.not_to be_allowed(:unlink) }
+ end
+ end
+ end
+end
diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb
index db3df760472..6d34b0a8b4b 100644
--- a/spec/policies/issuable_policy_spec.rb
+++ b/spec/policies/issuable_policy_spec.rb
@@ -13,7 +13,7 @@ describe IssuablePolicy, models: true do
context 'when user is able to read project' do
it 'enables user to read and update issuables' do
- expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request)
+ expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request, :reopen_merge_request)
end
end
@@ -24,12 +24,12 @@ describe IssuablePolicy, models: true do
it 'enables user to read and update issuables' do
project.add_maintainer(user)
- expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request)
+ expect(policies).to be_allowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request, :reopen_merge_request)
end
end
it 'disallows user from reading and updating issuables from that project' do
- expect(policies).to be_disallowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request)
+ expect(policies).to be_disallowed(:read_issue, :update_issue, :reopen_issue, :read_merge_request, :update_merge_request, :reopen_merge_request)
end
end
end
diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb
index 008d118b557..b149dbcf871 100644
--- a/spec/policies/issue_policy_spec.rb
+++ b/spec/policies/issue_policy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe IssuePolicy do
+ include ExternalAuthorizationServiceHelpers
+
let(:guest) { create(:user) }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
@@ -204,4 +206,21 @@ describe IssuePolicy do
end
end
end
+
+ context 'with external authorization enabled' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+ let(:policies) { described_class.new(user, issue) }
+
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'can read the issue iid without accessing the external service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(policies).to be_allowed(:read_issue_iid)
+ end
+ end
end
diff --git a/spec/policies/merge_request_policy_spec.rb b/spec/policies/merge_request_policy_spec.rb
new file mode 100644
index 00000000000..81279225d61
--- /dev/null
+++ b/spec/policies/merge_request_policy_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe MergeRequestPolicy do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:guest) { create(:user) }
+ let(:author) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ def permissions(user, merge_request)
+ described_class.new(user, merge_request)
+ end
+
+ before do
+ project.add_guest(guest)
+ project.add_guest(author)
+ project.add_developer(developer)
+ end
+
+ context 'when merge request is unlocked' do
+ let(:merge_request) { create(:merge_request, :closed, source_project: project, target_project: project, author: author) }
+
+ it 'allows author to reopen merge request' do
+ expect(permissions(author, merge_request)).to be_allowed(:reopen_merge_request)
+ end
+
+ it 'allows developer to reopen merge request' do
+ expect(permissions(developer, merge_request)).to be_allowed(:reopen_merge_request)
+ end
+
+ it 'prevents guest from reopening merge request' do
+ expect(permissions(guest, merge_request)).to be_disallowed(:reopen_merge_request)
+ end
+ end
+
+ context 'when merge request is locked' do
+ let(:merge_request_locked) { create(:merge_request, :closed, discussion_locked: true, source_project: project, target_project: project, author: author) }
+
+ it 'prevents author from reopening merge request' do
+ expect(permissions(author, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+ end
+
+ it 'prevents developer from reopening merge request' do
+ expect(permissions(developer, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+ end
+
+ it 'prevents guests from reopening merge request' do
+ expect(permissions(guest, merge_request_locked)).to be_disallowed(:reopen_merge_request)
+ end
+ end
+
+ context 'with external authorization enabled' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:policies) { described_class.new(user, merge_request) }
+
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'can read the issue iid without accessing the external service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(policies).to be_allowed(:read_merge_request_iid)
+ end
+ end
+end
diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb
index 1fdf95ad716..99fa8b1fe44 100644
--- a/spec/policies/namespace_policy_spec.rb
+++ b/spec/policies/namespace_policy_spec.rb
@@ -30,7 +30,7 @@ describe NamespacePolicy do
context 'user who has exceeded project limit' do
let(:owner) { create(:user, projects_limit: 0) }
- it { is_expected.not_to be_allowed(:create_projects) }
+ it { is_expected.to be_disallowed(:create_projects) }
end
end
diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb
index 0e848c74659..4be7a0266d1 100644
--- a/spec/policies/note_policy_spec.rb
+++ b/spec/policies/note_policy_spec.rb
@@ -1,28 +1,15 @@
require 'spec_helper'
-describe NotePolicy, mdoels: true do
+describe NotePolicy do
describe '#rules' do
let(:user) { create(:user) }
let(:project) { create(:project, :public) }
let(:issue) { create(:issue, project: project) }
-
- def policies(noteable = nil)
- return @policies if @policies
-
- noteable ||= issue
- note = if noteable.is_a?(Commit)
- create(:note_on_commit, commit_id: noteable.id, author: user, project: project)
- else
- create(:note, noteable: noteable, author: user, project: project)
- end
-
- @policies = described_class.new(user, note)
- end
+ let(:noteable) { issue }
+ let(:policy) { described_class.new(user, note) }
+ let(:note) { create(:note, noteable: noteable, author: user, project: project) }
shared_examples_for 'a discussion with a private noteable' do
- let(:noteable) { issue }
- let(:policy) { policies(noteable) }
-
context 'when the note author can no longer see the noteable' do
it 'can not edit nor read the note' do
expect(policy).to be_disallowed(:admin_note)
@@ -46,12 +33,21 @@ describe NotePolicy, mdoels: true do
end
end
- context 'when the project is private' do
- let(:project) { create(:project, :private, :repository) }
+ context 'when the noteable is a commit' do
+ let(:commit) { project.repository.head_commit }
+ let(:note) { create(:note_on_commit, commit_id: commit.id, author: user, project: project) }
+
+ context 'when the project is private' do
+ let(:project) { create(:project, :private, :repository) }
+
+ it_behaves_like 'a discussion with a private noteable'
+ end
- context 'when the noteable is a commit' do
- it_behaves_like 'a discussion with a private noteable' do
- let(:noteable) { project.repository.head_commit }
+ context 'when the project is public' do
+ context 'when repository access level is private' do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it_behaves_like 'a discussion with a private noteable'
end
end
end
@@ -59,44 +55,44 @@ describe NotePolicy, mdoels: true do
context 'when the project is public' do
context 'when the note author is not a project member' do
it 'can edit a note' do
- expect(policies).to be_allowed(:admin_note)
- expect(policies).to be_allowed(:resolve_note)
- expect(policies).to be_allowed(:read_note)
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
end
end
context 'when the noteable is a project snippet' do
- it 'can edit note' do
- policies = policies(create(:project_snippet, :public, project: project))
+ let(:noteable) { create(:project_snippet, :public, project: project) }
- expect(policies).to be_allowed(:admin_note)
- expect(policies).to be_allowed(:resolve_note)
- expect(policies).to be_allowed(:read_note)
+ it 'can edit note' do
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
end
context 'when it is private' do
- it_behaves_like 'a discussion with a private noteable' do
- let(:noteable) { create(:project_snippet, :private, project: project) }
- end
+ let(:noteable) { create(:project_snippet, :private, project: project) }
+
+ it_behaves_like 'a discussion with a private noteable'
end
end
context 'when the noteable is a personal snippet' do
- it 'can edit note' do
- policies = policies(create(:personal_snippet, :public))
+ let(:noteable) { create(:personal_snippet, :public) }
- expect(policies).to be_allowed(:admin_note)
- expect(policies).to be_allowed(:resolve_note)
- expect(policies).to be_allowed(:read_note)
+ it 'can edit note' do
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
end
context 'when it is private' do
- it 'can not edit nor read the note' do
- policies = policies(create(:personal_snippet, :private))
+ let(:noteable) { create(:personal_snippet, :private) }
- expect(policies).to be_disallowed(:admin_note)
- expect(policies).to be_disallowed(:resolve_note)
- expect(policies).to be_disallowed(:read_note)
+ it 'can not edit nor read the note' do
+ expect(policy).to be_disallowed(:admin_note)
+ expect(policy).to be_disallowed(:resolve_note)
+ expect(policy).to be_disallowed(:read_note)
end
end
end
@@ -120,20 +116,20 @@ describe NotePolicy, mdoels: true do
end
it 'can edit a note' do
- expect(policies).to be_allowed(:admin_note)
- expect(policies).to be_allowed(:resolve_note)
- expect(policies).to be_allowed(:read_note)
+ expect(policy).to be_allowed(:admin_note)
+ expect(policy).to be_allowed(:resolve_note)
+ expect(policy).to be_allowed(:read_note)
end
end
context 'when the note author is not a project member' do
it 'can not edit a note' do
- expect(policies).to be_disallowed(:admin_note)
- expect(policies).to be_disallowed(:resolve_note)
+ expect(policy).to be_disallowed(:admin_note)
+ expect(policy).to be_disallowed(:resolve_note)
end
it 'can read a note' do
- expect(policies).to be_allowed(:read_note)
+ expect(policy).to be_allowed(:read_note)
end
end
end
diff --git a/spec/policies/personal_snippet_policy_spec.rb b/spec/policies/personal_snippet_policy_spec.rb
index a38e0dbd797..097000ceb6a 100644
--- a/spec/policies/personal_snippet_policy_spec.rb
+++ b/spec/policies/personal_snippet_policy_spec.rb
@@ -14,13 +14,6 @@ describe PersonalSnippetPolicy do
]
end
- let(:comment_permissions) do
- [
- :comment_personal_snippet,
- :create_note
- ]
- end
-
def permissions(user)
described_class.new(user, snippet)
end
@@ -33,7 +26,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -44,7 +37,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -55,7 +48,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
@@ -70,7 +63,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -81,7 +74,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -92,7 +85,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -103,7 +96,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
@@ -118,7 +111,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -129,7 +122,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -140,7 +133,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_disallowed(:comment_personal_snippet)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -151,7 +144,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_disallowed(:read_personal_snippet)
- is_expected.to be_disallowed(*comment_permissions)
+ is_expected.to be_disallowed(:create_note)
is_expected.to be_disallowed(:award_emoji)
is_expected.to be_disallowed(*author_permissions)
end
@@ -162,7 +155,7 @@ describe PersonalSnippetPolicy do
it do
is_expected.to be_allowed(:read_personal_snippet)
- is_expected.to be_allowed(*comment_permissions)
+ is_expected.to be_allowed(:create_note)
is_expected.to be_allowed(:award_emoji)
is_expected.to be_allowed(*author_permissions)
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 93a468f585b..8075fcade5f 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe ProjectPolicy do
+ include ExternalAuthorizationServiceHelpers
+ include_context 'ProjectPolicy context'
set(:guest) { create(:user) }
set(:reporter) { create(:user) }
set(:developer) { create(:user) }
@@ -45,10 +47,10 @@ describe ProjectPolicy do
let(:base_maintainer_permissions) do
%i[
push_to_delete_protected_branch update_project_snippet update_environment
- update_deployment admin_project_snippet
- admin_project_member admin_note admin_wiki admin_project
+ update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
+ daily_statistics
]
end
@@ -130,22 +132,26 @@ describe ProjectPolicy do
subject { described_class.new(owner, project) }
context 'when the feature is disabled' do
- it 'does not include the issues permissions' do
+ before do
project.issues_enabled = false
project.save!
+ end
+ it 'does not include the issues permissions' do
expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue
end
- end
- context 'when the feature is disabled and external tracker configured' do
- it 'does not include the issues permissions' do
- create(:jira_service, project: project)
+ it 'disables boards and lists permissions' do
+ expect_disallowed :read_board, :create_board, :update_board
+ expect_disallowed :read_list, :create_list, :update_list, :admin_list
+ end
- project.issues_enabled = false
- project.save!
+ context 'when external tracker configured' do
+ it 'does not include the issues permissions' do
+ create(:jira_service, project: project)
- expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue
+ expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue
+ end
end
end
end
@@ -233,237 +239,6 @@ describe ProjectPolicy do
end
end
- shared_examples 'archived project policies' do
- let(:feature_write_abilities) do
- described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature|
- described_class.create_update_admin_destroy(feature)
- end
- end
-
- let(:other_write_abilities) do
- %i[
- create_merge_request_in
- create_merge_request_from
- push_to_delete_protected_branch
- push_code
- request_access
- upload_file
- resolve_note
- award_emoji
- ]
- end
-
- context 'when the project is archived' do
- before do
- project.archived = true
- end
-
- it 'disables write actions on all relevant project features' do
- expect_disallowed(*feature_write_abilities)
- end
-
- it 'disables some other important write actions' do
- expect_disallowed(*other_write_abilities)
- end
-
- it 'does not disable other abilities' do
- expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities))
- end
- end
- end
-
- shared_examples 'project policies as anonymous' do
- context 'abilities for public projects' do
- context 'when a project has pending invites' do
- let(:group) { create(:group, :public) }
- let(:project) { create(:project, :public, namespace: group) }
- let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] }
- let(:anonymous_permissions) { guest_permissions - user_permissions }
-
- subject { described_class.new(nil, project) }
-
- before do
- create(:group_member, :invited, group: group)
- end
-
- it 'does not grant owner access' do
- expect_allowed(*anonymous_permissions)
- expect_disallowed(*user_permissions)
- end
-
- it_behaves_like 'archived project policies' do
- let(:regular_abilities) { anonymous_permissions }
- end
- end
- end
-
- context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(nil, project) }
-
- it { is_expected.to be_banned }
- end
- end
-
- shared_examples 'project policies as guest' do
- subject { described_class.new(guest, project) }
-
- context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
- let(:reporter_public_build_permissions) do
- reporter_permissions - [:read_build, :read_pipeline]
- end
-
- it do
- expect_allowed(*guest_permissions)
- expect_disallowed(*reporter_public_build_permissions)
- expect_disallowed(*team_member_reporter_permissions)
- expect_disallowed(*developer_permissions)
- expect_disallowed(*maintainer_permissions)
- expect_disallowed(*owner_permissions)
- end
-
- it_behaves_like 'archived project policies' do
- let(:regular_abilities) { guest_permissions }
- end
-
- context 'public builds enabled' do
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(:read_build, :read_pipeline)
- end
- end
-
- context 'when public builds disabled' do
- before do
- project.update(public_builds: false)
- end
-
- it do
- expect_allowed(*guest_permissions)
- expect_disallowed(:read_build, :read_pipeline)
- end
- end
-
- context 'when builds are disabled' do
- before do
- project.project_feature.update(builds_access_level: ProjectFeature::DISABLED)
- end
-
- it do
- expect_disallowed(:read_build)
- expect_allowed(:read_pipeline)
- end
- end
- end
- end
-
- shared_examples 'project policies as reporter' do
- context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(reporter, project) }
-
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(*reporter_permissions)
- expect_allowed(*team_member_reporter_permissions)
- expect_disallowed(*developer_permissions)
- expect_disallowed(*maintainer_permissions)
- expect_disallowed(*owner_permissions)
- end
-
- it_behaves_like 'archived project policies' do
- let(:regular_abilities) { reporter_permissions }
- end
- end
- end
-
- shared_examples 'project policies as developer' do
- context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(developer, project) }
-
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(*reporter_permissions)
- expect_allowed(*team_member_reporter_permissions)
- expect_allowed(*developer_permissions)
- expect_disallowed(*maintainer_permissions)
- expect_disallowed(*owner_permissions)
- end
-
- it_behaves_like 'archived project policies' do
- let(:regular_abilities) { developer_permissions }
- end
- end
- end
-
- shared_examples 'project policies as maintainer' do
- context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(maintainer, project) }
-
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(*reporter_permissions)
- expect_allowed(*team_member_reporter_permissions)
- expect_allowed(*developer_permissions)
- expect_allowed(*maintainer_permissions)
- expect_disallowed(*owner_permissions)
- end
-
- it_behaves_like 'archived project policies' do
- let(:regular_abilities) { maintainer_permissions }
- end
- end
- end
-
- shared_examples 'project policies as owner' do
- context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(owner, project) }
-
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(*reporter_permissions)
- expect_allowed(*team_member_reporter_permissions)
- expect_allowed(*developer_permissions)
- expect_allowed(*maintainer_permissions)
- expect_allowed(*owner_permissions)
- end
-
- it_behaves_like 'archived project policies' do
- let(:regular_abilities) { owner_permissions }
- end
- end
- end
-
- shared_examples 'project policies as admin' do
- context 'abilities for non-public projects' do
- let(:project) { create(:project, namespace: owner.namespace) }
-
- subject { described_class.new(admin, project) }
-
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(*reporter_permissions)
- expect_disallowed(*team_member_reporter_permissions)
- expect_allowed(*developer_permissions)
- expect_allowed(*maintainer_permissions)
- expect_allowed(*owner_permissions)
- end
-
- it_behaves_like 'archived project policies' do
- let(:regular_abilities) { owner_permissions }
- end
- end
- end
-
it_behaves_like 'project policies as anonymous'
it_behaves_like 'project policies as guest'
it_behaves_like 'project policies as reporter'
@@ -518,4 +293,56 @@ describe ProjectPolicy do
projects: [clusterable])
end
end
+
+ context 'reading a project' do
+ it 'allows access when a user has read access to the repo' do
+ expect(described_class.new(owner, project)).to be_allowed(:read_project)
+ expect(described_class.new(developer, project)).to be_allowed(:read_project)
+ expect(described_class.new(admin, project)).to be_allowed(:read_project)
+ end
+
+ it 'never checks the external service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(described_class.new(owner, project)).to be_allowed(:read_project)
+ end
+
+ context 'with an external authorization service' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'allows access when the external service allows it' do
+ external_service_allow_access(owner, project)
+ external_service_allow_access(developer, project)
+
+ expect(described_class.new(owner, project)).to be_allowed(:read_project)
+ expect(described_class.new(developer, project)).to be_allowed(:read_project)
+ end
+
+ it 'does not check the external service for admins and allows access' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(described_class.new(admin, project)).to be_allowed(:read_project)
+ end
+
+ it 'prevents all but seeing a public project in a list when access is denied' do
+ [developer, owner, build(:user), nil].each do |user|
+ external_service_deny_access(user, project)
+ policy = described_class.new(user, project)
+
+ expect(policy).not_to be_allowed(:read_project)
+ expect(policy).not_to be_allowed(:owner_access)
+ expect(policy).not_to be_allowed(:change_namespace)
+ end
+ end
+
+ it 'passes the full path to external authorization for logging purposes' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(owner, 'default_label', project.full_path).and_call_original
+
+ described_class.new(owner, project).allowed?(:read_project)
+ end
+ end
+ end
end
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index d6329e84579..2e9ef1e89fd 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -5,7 +5,7 @@ describe ProjectSnippetPolicy do
let(:regular_user) { create(:user) }
let(:external_user) { create(:user, :external) }
let(:project) { create(:project, :public) }
-
+ let(:snippet) { create(:project_snippet, snippet_visibility, project: project) }
let(:author_permissions) do
[
:update_project_snippet,
@@ -13,23 +13,13 @@ describe ProjectSnippetPolicy do
]
end
- def abilities(user, snippet_visibility)
- snippet = create(:project_snippet, snippet_visibility, project: project)
-
- described_class.new(user, snippet)
- end
-
- def expect_allowed(*permissions)
- permissions.each { |p| is_expected.to be_allowed(p) }
- end
-
- def expect_disallowed(*permissions)
- permissions.each { |p| is_expected.not_to be_allowed(p) }
- end
+ subject { described_class.new(current_user, snippet) }
context 'public snippet' do
+ let(:snippet_visibility) { :public }
+
context 'no user' do
- subject { abilities(nil, :public) }
+ let(:current_user) { nil }
it do
expect_allowed(:read_project_snippet)
@@ -38,7 +28,7 @@ describe ProjectSnippetPolicy do
end
context 'regular user' do
- subject { abilities(regular_user, :public) }
+ let(:current_user) { regular_user }
it do
expect_allowed(:read_project_snippet, :create_note)
@@ -47,7 +37,7 @@ describe ProjectSnippetPolicy do
end
context 'external user' do
- subject { abilities(external_user, :public) }
+ let(:current_user) { external_user }
it do
expect_allowed(:read_project_snippet, :create_note)
@@ -57,8 +47,10 @@ describe ProjectSnippetPolicy do
end
context 'internal snippet' do
+ let(:snippet_visibility) { :internal }
+
context 'no user' do
- subject { abilities(nil, :internal) }
+ let(:current_user) { nil }
it do
expect_disallowed(:read_project_snippet)
@@ -67,7 +59,7 @@ describe ProjectSnippetPolicy do
end
context 'regular user' do
- subject { abilities(regular_user, :internal) }
+ let(:current_user) { regular_user }
it do
expect_allowed(:read_project_snippet, :create_note)
@@ -76,31 +68,31 @@ describe ProjectSnippetPolicy do
end
context 'external user' do
- subject { abilities(external_user, :internal) }
+ let(:current_user) { external_user }
it do
expect_disallowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
- end
- context 'project team member external user' do
- subject { abilities(external_user, :internal) }
-
- before do
- project.add_developer(external_user)
- end
+ context 'project team member' do
+ before do
+ project.add_developer(external_user)
+ end
- it do
- expect_allowed(:read_project_snippet, :create_note)
- expect_disallowed(*author_permissions)
+ it do
+ expect_allowed(:read_project_snippet, :create_note)
+ expect_disallowed(*author_permissions)
+ end
end
end
end
context 'private snippet' do
+ let(:snippet_visibility) { :private }
+
context 'no user' do
- subject { abilities(nil, :private) }
+ let(:current_user) { nil }
it do
expect_disallowed(:read_project_snippet)
@@ -109,53 +101,52 @@ describe ProjectSnippetPolicy do
end
context 'regular user' do
- subject { abilities(regular_user, :private) }
+ let(:current_user) { regular_user }
it do
expect_disallowed(:read_project_snippet, :create_note)
expect_disallowed(*author_permissions)
end
- end
-
- context 'snippet author' do
- let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) }
- subject { described_class.new(regular_user, snippet) }
+ context 'snippet author' do
+ let(:snippet) { create(:project_snippet, :private, author: regular_user, project: project) }
- it do
- expect_allowed(:read_project_snippet, :create_note)
- expect_allowed(*author_permissions)
+ it do
+ expect_allowed(:read_project_snippet, :create_note)
+ expect_allowed(*author_permissions)
+ end
end
- end
- context 'project team member normal user' do
- subject { abilities(regular_user, :private) }
-
- before do
- project.add_developer(regular_user)
- end
+ context 'project team member normal user' do
+ before do
+ project.add_developer(regular_user)
+ end
- it do
- expect_allowed(:read_project_snippet, :create_note)
- expect_disallowed(*author_permissions)
+ it do
+ expect_allowed(:read_project_snippet, :create_note)
+ expect_disallowed(*author_permissions)
+ end
end
end
- context 'project team member external user' do
- subject { abilities(external_user, :private) }
+ context 'external user' do
+ context 'project team member' do
+ let(:current_user) { external_user }
- before do
- project.add_developer(external_user)
- end
+ before do
+ project.add_developer(external_user)
+ end
- it do
- expect_allowed(:read_project_snippet, :create_note)
- expect_disallowed(*author_permissions)
+ it do
+ expect_allowed(:read_project_snippet, :create_note)
+ expect_disallowed(*author_permissions)
+ end
end
end
context 'admin user' do
- subject { abilities(create(:admin), :private) }
+ let(:snippet_visibility) { :private }
+ let(:current_user) { create(:admin) }
it do
expect_allowed(:read_project_snippet, :create_note)
diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb
index bb1db9a3d51..eacf383be7d 100644
--- a/spec/presenters/blob_presenter_spec.rb
+++ b/spec/presenters/blob_presenter_spec.rb
@@ -14,6 +14,16 @@ describe BlobPresenter, :seed_helper do
end
let(:blob) { Blob.new(git_blob) }
+ describe '.web_url' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.blobs.first, repository) }
+
+ subject { described_class.new(blob) }
+
+ it { expect(subject.web_url).to eq("http://localhost/#{project.full_path}/blob/#{blob.commit_id}/#{blob.path}") }
+ end
+
describe '#highlight' do
subject { described_class.new(blob) }
diff --git a/spec/presenters/blobs/unfold_presenter_spec.rb b/spec/presenters/blobs/unfold_presenter_spec.rb
new file mode 100644
index 00000000000..7ece5f623ce
--- /dev/null
+++ b/spec/presenters/blobs/unfold_presenter_spec.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Blobs::UnfoldPresenter do
+ include FakeBlobHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:blob) { fake_blob(path: 'foo', data: "1\n2\n3") }
+ let(:subject) { described_class.new(blob, params) }
+
+ describe '#initialize' do
+ context 'when full is false' do
+ let(:params) { { full: false, since: 2, to: 3, bottom: false, offset: 1, indent: 1 } }
+
+ it 'sets attributes' do
+ result = subject
+
+ expect(result.full?).to eq(false)
+ expect(result.since).to eq(2)
+ expect(result.to).to eq(3)
+ expect(result.bottom).to eq(false)
+ expect(result.offset).to eq(1)
+ expect(result.indent).to eq(1)
+ end
+ end
+
+ context 'when full is true' do
+ let(:params) { { full: true, since: 2, to: 3, bottom: false, offset: 1, indent: 1 } }
+
+ it 'sets other attributes' do
+ result = subject
+
+ expect(result.full?).to eq(true)
+ expect(result.since).to eq(1)
+ expect(result.to).to eq(blob.lines.size)
+ expect(result.bottom).to eq(false)
+ expect(result.offset).to eq(0)
+ expect(result.indent).to eq(0)
+ end
+ end
+ end
+
+ describe '#diff_lines' do
+ let(:total_lines) { 50 }
+ let(:blob) { fake_blob(path: 'foo', data: (1..total_lines).to_a.join("\n")) }
+
+ context 'when "full" is true' do
+ let(:params) { { full: true } }
+
+ it 'returns all lines' do
+ lines = subject.diff_lines
+
+ expect(lines.size).to eq(total_lines)
+
+ lines.each.with_index do |line, index|
+ expect(line.text).to include("LC#{index + 1}")
+ expect(line.text).to eq(line.rich_text)
+ expect(line.type).to be_nil
+ end
+ end
+
+ context 'when last line is empty' do
+ let(:blob) { fake_blob(path: 'foo', data: "1\n2\n") }
+
+ it 'disregards last line' do
+ lines = subject.diff_lines
+
+ expect(lines.size).to eq(2)
+ end
+ end
+ end
+
+ context 'when "since" is equal to 1' do
+ let(:params) { { since: 1, to: 10, offset: 10 } }
+
+ it 'does not add top match line' do
+ line = subject.diff_lines.first
+
+ expect(line.type).to be_nil
+ end
+ end
+
+ context 'when since is greater than 1' do
+ let(:params) { { since: 5, to: 10, offset: 10 } }
+
+ it 'adds top match line' do
+ line = subject.diff_lines.first
+
+ expect(line.type).to eq('match')
+ expect(line.old_pos).to eq(5)
+ expect(line.new_pos).to eq(5)
+ end
+ end
+
+ context 'when "to" is less than blob size' do
+ let(:params) { { since: 1, to: 5, offset: 10, bottom: true } }
+
+ it 'adds bottom match line' do
+ line = subject.diff_lines.last
+
+ expect(line.type).to eq('match')
+ expect(line.old_pos).to eq(-5)
+ expect(line.new_pos).to eq(5)
+ end
+ end
+
+ context 'when "to" is equal to blob size' do
+ let(:params) { { since: 1, to: total_lines, offset: 10, bottom: true } }
+
+ it 'does not add bottom match line' do
+ line = subject.diff_lines.last
+
+ expect(line.type).to be_nil
+ end
+ end
+ end
+
+ describe '#lines' do
+ context 'when scope is specified' do
+ let(:params) { { since: 2, to: 3 } }
+
+ it 'returns lines cropped by params' do
+ expect(subject.lines.size).to eq(2)
+ expect(subject.lines[0]).to include('LC2')
+ expect(subject.lines[1]).to include('LC3')
+ end
+ end
+
+ context 'when full is true' do
+ let(:params) { { full: true } }
+
+ it 'returns all lines' do
+ expect(subject.lines.size).to eq(3)
+ expect(subject.lines[0]).to include('LC1')
+ expect(subject.lines[1]).to include('LC2')
+ expect(subject.lines[2]).to include('LC3')
+ end
+ end
+ end
+
+ describe '#match_line_text' do
+ context 'when bottom is true' do
+ let(:params) { { since: 2, to: 3, bottom: true } }
+
+ it 'returns empty string' do
+ expect(subject.match_line_text).to eq('')
+ end
+ end
+
+ context 'when bottom is false' do
+ let(:params) { { since: 2, to: 3, bottom: false } }
+
+ it 'returns match line string' do
+ expect(subject.match_line_text).to eq("@@ -2,1+2,1 @@")
+ end
+ end
+ end
+end
diff --git a/spec/presenters/ci/bridge_presenter_spec.rb b/spec/presenters/ci/bridge_presenter_spec.rb
new file mode 100644
index 00000000000..986818a7b9e
--- /dev/null
+++ b/spec/presenters/ci/bridge_presenter_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Ci::BridgePresenter do
+ set(:project) { create(:project) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+ set(:bridge) { create(:ci_bridge, pipeline: pipeline, status: :failed) }
+
+ subject(:presenter) do
+ described_class.new(bridge)
+ end
+
+ it 'presents information about recoverable state' do
+ expect(presenter).to be_recoverable
+ end
+end
diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb
index 676835b3880..e202f7a9b5f 100644
--- a/spec/presenters/ci/build_presenter_spec.rb
+++ b/spec/presenters/ci/build_presenter_spec.rb
@@ -180,7 +180,7 @@ describe Ci::BuildPresenter do
context 'When build has failed and retried' do
let(:build) { create(:ci_build, :script_failure, :retried, pipeline: pipeline) }
- it 'should include the reason of failure and the retried title' do
+ it 'includes the reason of failure and the retried title' do
tooltip = subject.tooltip_message
expect(tooltip).to eq("#{build.name} - failed - (script failure) (retried)")
@@ -190,7 +190,7 @@ describe Ci::BuildPresenter do
context 'When build has failed and is allowed to' do
let(:build) { create(:ci_build, :script_failure, :allowed_to_fail, pipeline: pipeline) }
- it 'should include the reason of failure' do
+ it 'includes the reason of failure' do
tooltip = subject.tooltip_message
expect(tooltip).to eq("#{build.name} - failed - (script failure) (allowed to fail)")
@@ -200,7 +200,7 @@ describe Ci::BuildPresenter do
context 'For any other build (no retried)' do
let(:build) { create(:ci_build, :success, pipeline: pipeline) }
- it 'should include build name and status' do
+ it 'includes build name and status' do
tooltip = subject.tooltip_message
expect(tooltip).to eq("#{build.name} - passed")
@@ -210,7 +210,7 @@ describe Ci::BuildPresenter do
context 'For any other build (retried)' do
let(:build) { create(:ci_build, :success, :retried, pipeline: pipeline) }
- it 'should include build name and status' do
+ it 'includes build name and status' do
tooltip = subject.tooltip_message
expect(tooltip).to eq("#{build.name} - passed (retried)")
@@ -269,7 +269,7 @@ describe Ci::BuildPresenter do
context 'when is a script or missing dependency failure' do
let(:failure_reasons) { %w(script_failure missing_dependency_failure archived_failure) }
- it 'should return false' do
+ it 'returns false' do
failure_reasons.each do |failure_reason|
build.update_attribute(:failure_reason, failure_reason)
expect(presenter.recoverable?).to be_falsy
@@ -280,7 +280,7 @@ describe Ci::BuildPresenter do
context 'when is any other failure type' do
let(:failure_reasons) { %w(unknown_failure api_failure stuck_or_timeout_failure runner_system_failure) }
- it 'should return true' do
+ it 'returns true' do
failure_reasons.each do |failure_reason|
build.update_attribute(:failure_reason, failure_reason)
expect(presenter.recoverable?).to be_truthy
diff --git a/spec/presenters/ci/build_runner_presenter_spec.rb b/spec/presenters/ci/build_runner_presenter_spec.rb
index f50bcf54b46..9ed8e3a4e0a 100644
--- a/spec/presenters/ci/build_runner_presenter_spec.rb
+++ b/spec/presenters/ci/build_runner_presenter_spec.rb
@@ -119,23 +119,23 @@ describe Ci::BuildRunnerPresenter do
end
describe '#git_depth' do
- subject { presenter.git_depth }
-
let(:build) { create(:ci_build) }
- it 'returns the correct git depth' do
- is_expected.to eq(0)
- end
+ subject(:git_depth) { presenter.git_depth }
context 'when GIT_DEPTH variable is specified' do
before do
create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: build.pipeline)
end
- it 'returns the correct git depth' do
- is_expected.to eq(1)
+ it 'returns its value' do
+ expect(git_depth).to eq(1)
end
end
+
+ it 'defaults to git depth setting for the project' do
+ expect(git_depth).to eq(build.project.default_git_depth)
+ end
end
describe '#refspecs' do
@@ -144,24 +144,56 @@ describe Ci::BuildRunnerPresenter do
let(:build) { create(:ci_build) }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly('+refs/tags/*:refs/tags/*',
- '+refs/heads/*:refs/remotes/origin/*')
+ is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
- context 'when GIT_DEPTH variable is specified' do
- before do
- create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 1, pipeline: build.pipeline)
+ context 'when ref is tag' do
+ let(:build) { create(:ci_build, :tag) }
+
+ it 'returns the correct refspecs' do
+ is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}")
end
+ context 'when GIT_DEPTH is zero' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
+ end
+
+ it 'returns the correct refspecs' do
+ is_expected.to contain_exactly('+refs/tags/*:refs/tags/*',
+ '+refs/heads/*:refs/remotes/origin/*')
+ end
+ end
+ end
+
+ context 'when pipeline is detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.all_pipelines.first }
+ let(:build) { create(:ci_build, ref: pipeline.ref, pipeline: pipeline) }
+
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
+ is_expected
+ .to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head')
+ end
+
+ context 'when GIT_DEPTH is zero' do
+ before do
+ create(:ci_pipeline_variable, key: 'GIT_DEPTH', value: 0, pipeline: build.pipeline)
+ end
+
+ it 'returns the correct refspecs' do
+ is_expected
+ .to contain_exactly('+refs/merge-requests/1/head:refs/merge-requests/1/head',
+ '+refs/heads/*:refs/remotes/origin/*',
+ '+refs/tags/*:refs/tags/*')
+ end
end
- context 'when ref is tag' do
- let(:build) { create(:ci_build, :tag) }
+ context 'when pipeline is legacy detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_legacy_detached_merge_request_pipeline) }
it 'returns the correct refspecs' do
- is_expected.to contain_exactly("+refs/tags/#{build.ref}:refs/tags/#{build.ref}")
+ is_expected.to contain_exactly("+refs/heads/#{build.ref}:refs/remotes/origin/#{build.ref}")
end
end
end
diff --git a/spec/presenters/ci/pipeline_presenter_spec.rb b/spec/presenters/ci/pipeline_presenter_spec.rb
index f7ceaf844be..cda07a0ae09 100644
--- a/spec/presenters/ci/pipeline_presenter_spec.rb
+++ b/spec/presenters/ci/pipeline_presenter_spec.rb
@@ -1,6 +1,9 @@
require 'spec_helper'
describe Ci::PipelinePresenter do
+ include Gitlab::Routing
+
+ let(:user) { create(:user) }
let(:project) { create(:project) }
let(:pipeline) { create(:ci_pipeline, project: project) }
@@ -8,6 +11,11 @@ describe Ci::PipelinePresenter do
described_class.new(pipeline)
end
+ before do
+ project.add_developer(user)
+ allow(presenter).to receive(:current_user) { user }
+ end
+
it 'inherits from Gitlab::View::Presenter::Delegated' do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
end
@@ -68,4 +76,130 @@ describe Ci::PipelinePresenter do
end
end
end
+
+ describe '#ref_text' do
+ subject { presenter.ref_text }
+
+ context 'when pipeline is detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.all_pipelines.last }
+
+ it 'returns a correct ref text' do
+ is_expected.to eq("for <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \
+ "with <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a>")
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+ let(:pipeline) { merge_request.all_pipelines.last }
+
+ it 'returns a correct ref text' do
+ is_expected.to eq("for <a class=\"mr-iid\" href=\"#{project_merge_request_path(merge_request.project, merge_request)}\">#{merge_request.to_reference}</a> " \
+ "with <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.source_project, merge_request.source_branch)}\">#{merge_request.source_branch}</a> " \
+ "into <a class=\"ref-name\" href=\"#{project_commits_path(merge_request.target_project, merge_request.target_branch)}\">#{merge_request.target_branch}</a>")
+ end
+ end
+
+ context 'when pipeline is branch pipeline' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when ref exists in the repository' do
+ before do
+ allow(pipeline).to receive(:ref_exists?) { true }
+ end
+
+ it 'returns a correct ref text' do
+ is_expected.to eq("for <a class=\"ref-name\" href=\"#{project_commits_path(pipeline.project, pipeline.ref)}\">#{pipeline.ref}</a>")
+ end
+
+ context 'when ref contains malicious script' do
+ let(:pipeline) { create(:ci_pipeline, ref: "<script>alter('1')</script>", project: project) }
+
+ it 'does not include the malicious script' do
+ is_expected.not_to include("<script>alter('1')</script>")
+ end
+ end
+ end
+
+ context 'when ref exists in the repository' do
+ before do
+ allow(pipeline).to receive(:ref_exists?) { false }
+ end
+
+ it 'returns a correct ref text' do
+ is_expected.to eq("for <span class=\"ref-name\">#{pipeline.ref}</span>")
+ end
+
+ context 'when ref contains malicious script' do
+ let(:pipeline) { create(:ci_pipeline, ref: "<script>alter('1')</script>", project: project) }
+
+ it 'does not include the malicious script' do
+ is_expected.not_to include("<script>alter('1')</script>")
+ end
+ end
+ end
+ end
+ end
+
+ describe '#link_to_merge_request' do
+ subject { presenter.link_to_merge_request }
+
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.all_pipelines.last }
+
+ it 'returns a correct link' do
+ is_expected
+ .to include(project_merge_request_path(merge_request.project, merge_request))
+ end
+
+ context 'when pipeline is branch pipeline' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#link_to_merge_request_source_branch' do
+ subject { presenter.link_to_merge_request_source_branch }
+
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:pipeline) { merge_request.all_pipelines.last }
+
+ it 'returns a correct link' do
+ is_expected
+ .to include(project_commits_path(merge_request.source_project,
+ merge_request.source_branch))
+ end
+
+ context 'when pipeline is branch pipeline' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#link_to_merge_request_target_branch' do
+ subject { presenter.link_to_merge_request_target_branch }
+
+ let(:merge_request) { create(:merge_request, :with_merge_request_pipeline) }
+ let(:pipeline) { merge_request.all_pipelines.last }
+
+ it 'returns a correct link' do
+ is_expected
+ .to include(project_commits_path(merge_request.target_project, merge_request.target_branch))
+ end
+
+ context 'when pipeline is branch pipeline' do
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ it 'returns nothing' do
+ is_expected.to be_nil
+ end
+ end
+ end
end
diff --git a/spec/presenters/clusters/cluster_presenter_spec.rb b/spec/presenters/clusters/cluster_presenter_spec.rb
index 754ba0a594c..7054a70e2ed 100644
--- a/spec/presenters/clusters/cluster_presenter_spec.rb
+++ b/spec/presenters/clusters/cluster_presenter_spec.rb
@@ -158,46 +158,6 @@ describe Clusters::ClusterPresenter do
it { is_expected.to include(cluster.name) }
end
- describe '#can_toggle_cluster' do
- let(:user) { create(:user) }
-
- before do
- allow(cluster).to receive(:current_user).and_return(user)
- end
-
- subject { described_class.new(cluster).can_toggle_cluster? }
-
- context 'when user can update' do
- before do
- allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(true)
- end
-
- context 'when cluster is created' do
- before do
- allow(cluster).to receive(:created?).and_return(true)
- end
-
- it { is_expected.to eq(true) }
- end
-
- context 'when cluster is not created' do
- before do
- allow(cluster).to receive(:created?).and_return(false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
- context 'when user can not update' do
- before do
- allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(false)
- end
-
- it { is_expected.to eq(false) }
- end
- end
-
describe '#cluster_type_description' do
subject { described_class.new(cluster).cluster_type_description }
@@ -210,6 +170,12 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq('Group cluster') }
end
+
+ context 'instance_type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { is_expected.to eq('Instance cluster') }
+ end
end
describe '#show_path' do
@@ -227,5 +193,27 @@ describe Clusters::ClusterPresenter do
it { is_expected.to eq(group_cluster_path(group, cluster)) }
end
+
+ context 'instance_type cluster' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, :instance) }
+
+ it { is_expected.to eq(admin_cluster_path(cluster)) }
+ end
+ end
+
+ describe '#read_only_kubernetes_platform_fields?' do
+ subject { described_class.new(cluster).read_only_kubernetes_platform_fields? }
+
+ context 'with a user-provided cluster' do
+ let(:cluster) { build_stubbed(:cluster, :provided_by_user) }
+
+ it { is_expected.to be_falsy }
+ end
+
+ context 'with a GCP-provided cluster' do
+ let(:cluster) { build_stubbed(:cluster, :provided_by_gcp) }
+
+ it { is_expected.to be_truthy }
+ end
end
end
diff --git a/spec/presenters/group_clusterable_presenter_spec.rb b/spec/presenters/group_clusterable_presenter_spec.rb
index 205160742bf..fa77273f6aa 100644
--- a/spec/presenters/group_clusterable_presenter_spec.rb
+++ b/spec/presenters/group_clusterable_presenter_spec.rb
@@ -69,6 +69,14 @@ describe GroupClusterablePresenter do
it { is_expected.to eq(install_applications_group_cluster_path(group, cluster, application)) }
end
+ describe '#update_applications_cluster_path' do
+ let(:application) { :helm }
+
+ subject { presenter.update_applications_cluster_path(cluster, application) }
+
+ it { is_expected.to eq(update_applications_group_cluster_path(group, cluster, application)) }
+ end
+
describe '#cluster_path' do
subject { presenter.cluster_path(cluster) }
diff --git a/spec/presenters/issue_presenter_spec.rb b/spec/presenters/issue_presenter_spec.rb
new file mode 100644
index 00000000000..8e24559341b
--- /dev/null
+++ b/spec/presenters/issue_presenter_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe IssuePresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:issue) { create(:issue, project: project) }
+ let(:presenter) { described_class.new(issue, current_user: user) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ describe '#web_url' do
+ it 'returns correct path' do
+ expect(presenter.web_url).to eq "http://localhost/#{group.name}/#{project.name}/issues/#{issue.iid}"
+ end
+ end
+
+ describe '#issue_path' do
+ it 'returns correct path' do
+ expect(presenter.issue_path).to eq "/#{group.name}/#{project.name}/issues/#{issue.iid}"
+ end
+ end
+end
diff --git a/spec/presenters/label_presenter_spec.rb b/spec/presenters/label_presenter_spec.rb
new file mode 100644
index 00000000000..d566da7c872
--- /dev/null
+++ b/spec/presenters/label_presenter_spec.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe LabelPresenter do
+ include Gitlab::Routing.url_helpers
+
+ set(:group) { create(:group) }
+ set(:project) { create(:project, group: group) }
+ let(:label) { build_stubbed(:label, project: project).present(issuable_subject: project) }
+ let(:group_label) { build_stubbed(:group_label, group: group).present(issuable_subject: project) }
+
+ describe '#edit_path' do
+ context 'with group label' do
+ subject { group_label.edit_path }
+
+ it { is_expected.to eq(edit_group_label_path(group, group_label)) }
+ end
+
+ context 'with project label' do
+ subject { label.edit_path }
+
+ it { is_expected.to eq(edit_project_label_path(project, label)) }
+ end
+ end
+
+ describe '#destroy_path' do
+ context 'with group label' do
+ subject { group_label.destroy_path }
+
+ it { is_expected.to eq(group_label_path(group, group_label)) }
+ end
+
+ context 'with project label' do
+ subject { label.destroy_path }
+
+ it { is_expected.to eq(project_label_path(project, label)) }
+ end
+ end
+
+ describe '#filter_path' do
+ context 'with group as context subject' do
+ let(:label_in_group) { build_stubbed(:label, project: project).present(issuable_subject: group) }
+ subject { label_in_group.filter_path }
+
+ it { is_expected.to eq(issues_group_path(group, label_name: [label_in_group.title])) }
+ end
+
+ context 'with project as context subject' do
+ subject { label.filter_path }
+
+ it { is_expected.to eq(namespace_project_issues_path(group, project, label_name: [label.title])) }
+ end
+ end
+
+ describe '#can_subscribe_to_label_in_different_levels?' do
+ it 'returns true for group labels in project context' do
+ expect(group_label.can_subscribe_to_label_in_different_levels?).to be_truthy
+ end
+
+ it 'returns false for project labels in project context' do
+ expect(label.can_subscribe_to_label_in_different_levels?).to be_falsey
+ end
+ end
+
+ describe '#project_label?' do
+ context 'with group label' do
+ subject { group_label.project_label? }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with project label' do
+ subject { label.project_label? }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ describe '#subject_name' do
+ context 'with group label' do
+ subject { group_label.subject_name }
+
+ it { is_expected.to eq(group_label.group.name) }
+ end
+
+ context 'with project label' do
+ subject { label.subject_name }
+
+ it { is_expected.to eq(label.project.name) }
+ end
+ end
+end
diff --git a/spec/presenters/merge_request_presenter_spec.rb b/spec/presenters/merge_request_presenter_spec.rb
index bafcddebbb7..6408b0bd748 100644
--- a/spec/presenters/merge_request_presenter_spec.rb
+++ b/spec/presenters/merge_request_presenter_spec.rb
@@ -1,8 +1,8 @@
require 'spec_helper'
describe MergeRequestPresenter do
- let(:resource) { create :merge_request, source_project: project }
- let(:project) { create :project }
+ let(:resource) { create(:merge_request, source_project: project) }
+ let(:project) { create(:project) }
let(:user) { create(:user) }
describe '#ci_status' do
@@ -40,8 +40,8 @@ describe MergeRequestPresenter do
allow(pipeline).to receive(:has_warnings?) { true }
end
- it 'returns "success_with_warnings"' do
- is_expected.to eq('success_with_warnings')
+ it 'returns "success-with-warnings"' do
+ is_expected.to eq('success-with-warnings')
end
end
@@ -207,25 +207,25 @@ describe MergeRequestPresenter do
end
end
- describe '#cancel_merge_when_pipeline_succeeds_path' do
+ describe '#cancel_auto_merge_path' do
subject do
described_class.new(resource, current_user: user)
- .cancel_merge_when_pipeline_succeeds_path
+ .cancel_auto_merge_path
end
context 'when can cancel mwps' do
it 'returns path' do
- allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
+ allow(resource).to receive(:can_cancel_auto_merge?)
.with(user)
.and_return(true)
- is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_merge_when_pipeline_succeeds")
+ is_expected.to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}/cancel_auto_merge")
end
end
context 'when cannot cancel mwps' do
it 'returns nil' do
- allow(resource).to receive(:can_cancel_merge_when_pipeline_succeeds?)
+ allow(resource).to receive(:can_cancel_auto_merge?)
.with(user)
.and_return(false)
@@ -345,6 +345,30 @@ describe MergeRequestPresenter do
end
end
+ describe '#source_branch_commits_path' do
+ subject do
+ described_class.new(resource, current_user: user)
+ .source_branch_commits_path
+ end
+
+ context 'when source branch exists' do
+ it 'returns path' do
+ allow(resource).to receive(:source_branch_exists?) { true }
+
+ is_expected
+ .to eq("/#{resource.source_project.full_path}/commits/#{resource.source_branch}")
+ end
+ end
+
+ context 'when source branch does not exist' do
+ it 'returns nil' do
+ allow(resource).to receive(:source_branch_exists?) { false }
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
describe '#target_branch_tree_path' do
subject do
described_class.new(resource, current_user: user)
@@ -379,7 +403,7 @@ describe MergeRequestPresenter do
allow(resource).to receive(:source_branch_exists?) { true }
is_expected
- .to eq("/#{resource.source_project.full_path}/branches/#{resource.source_branch}")
+ .to eq("/#{resource.source_project.full_path}/-/branches/#{resource.source_branch}")
end
end
@@ -392,6 +416,75 @@ describe MergeRequestPresenter do
end
end
+ describe '#target_branch_path' do
+ subject do
+ described_class.new(resource, current_user: user).target_branch_path
+ end
+
+ context 'when target branch exists' do
+ it 'returns path' do
+ allow(resource).to receive(:target_branch_exists?) { true }
+
+ is_expected
+ .to eq("/#{resource.source_project.full_path}/-/branches/#{resource.target_branch}")
+ end
+ end
+
+ context 'when target branch does not exist' do
+ it 'returns nil' do
+ allow(resource).to receive(:target_branch_exists?) { false }
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#source_branch_link' do
+ subject { presenter.source_branch_link }
+
+ let(:presenter) { described_class.new(resource, current_user: user) }
+
+ context 'when source branch exists' do
+ it 'returns link' do
+ allow(resource).to receive(:source_branch_exists?) { true }
+
+ is_expected
+ .to eq("<a class=\"ref-name\" href=\"#{presenter.source_branch_commits_path}\">#{presenter.source_branch}</a>")
+ end
+ end
+
+ context 'when source branch does not exist' do
+ it 'returns text' do
+ allow(resource).to receive(:source_branch_exists?) { false }
+
+ is_expected.to eq("<span class=\"ref-name\">#{presenter.source_branch}</span>")
+ end
+ end
+ end
+
+ describe '#target_branch_link' do
+ subject { presenter.target_branch_link }
+
+ let(:presenter) { described_class.new(resource, current_user: user) }
+
+ context 'when target branch exists' do
+ it 'returns link' do
+ allow(resource).to receive(:target_branch_exists?) { true }
+
+ is_expected
+ .to eq("<a class=\"ref-name\" href=\"#{presenter.target_branch_commits_path}\">#{presenter.target_branch}</a>")
+ end
+ end
+
+ context 'when target branch does not exist' do
+ it 'returns text' do
+ allow(resource).to receive(:target_branch_exists?) { false }
+
+ is_expected.to eq("<span class=\"ref-name\">#{presenter.target_branch}</span>")
+ end
+ end
+ end
+
describe '#source_branch_with_namespace_link' do
subject do
described_class.new(resource, current_user: user).source_branch_with_namespace_link
@@ -476,4 +569,46 @@ describe MergeRequestPresenter do
end
end
end
+
+ describe '#can_push_to_source_branch' do
+ before do
+ allow(resource).to receive(:source_branch_exists?) { source_branch_exists }
+
+ allow_any_instance_of(Gitlab::UserAccess::RequestCacheExtension)
+ .to receive(:can_push_to_branch?)
+ .with(resource.source_branch)
+ .and_return(can_push_to_branch)
+ end
+
+ subject do
+ described_class.new(resource, current_user: user).can_push_to_source_branch?
+ end
+
+ context 'when source branch exists AND user can push to source branch' do
+ let(:source_branch_exists) { true }
+ let(:can_push_to_branch) { true }
+
+ it 'returns true' do
+ is_expected.to eq(true)
+ end
+ end
+
+ context 'when source branch does not exists' do
+ let(:source_branch_exists) { false }
+ let(:can_push_to_branch) { true }
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ end
+ end
+
+ context 'when user cannot push to source branch' do
+ let(:source_branch_exists) { true }
+ let(:can_push_to_branch) { false }
+
+ it 'returns false' do
+ is_expected.to eq(false)
+ end
+ end
+ end
end
diff --git a/spec/presenters/project_clusterable_presenter_spec.rb b/spec/presenters/project_clusterable_presenter_spec.rb
index c50d90ae1e8..6786a84243f 100644
--- a/spec/presenters/project_clusterable_presenter_spec.rb
+++ b/spec/presenters/project_clusterable_presenter_spec.rb
@@ -69,6 +69,14 @@ describe ProjectClusterablePresenter do
it { is_expected.to eq(install_applications_project_cluster_path(project, cluster, application)) }
end
+ describe '#update_applications_cluster_path' do
+ let(:application) { :helm }
+
+ subject { presenter.update_applications_cluster_path(cluster, application) }
+
+ it { is_expected.to eq(update_applications_project_cluster_path(project, cluster, application)) }
+ end
+
describe '#cluster_path' do
subject { presenter.cluster_path(cluster) }
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 456de5f1b9a..5bf80f6e318 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -411,4 +411,23 @@ describe ProjectPresenter do
end
end
end
+
+ describe '#statistics_buttons' do
+ let(:project) { build(:project) }
+ let(:presenter) { described_class.new(project, current_user: user) }
+
+ it 'orders the items correctly' do
+ allow(project.repository).to receive(:readme).and_return(double(name: 'readme'))
+ allow(project.repository).to receive(:changelog).and_return(nil)
+ allow(project.repository).to receive(:contribution_guide).and_return(double(name: 'foo'))
+ allow(presenter).to receive(:filename_path).and_return('fake/path')
+ allow(presenter).to receive(:contribution_guide_path).and_return('fake_path')
+
+ buttons = presenter.statistics_buttons(show_auto_devops_callout: false)
+ expect(buttons.map(&:label)).to start_with(
+ a_string_including('README'),
+ a_string_including('CONTRIBUTING')
+ )
+ end
+ end
end
diff --git a/spec/presenters/tree_entry_presenter_spec.rb b/spec/presenters/tree_entry_presenter_spec.rb
new file mode 100644
index 00000000000..d74ee5dc28f
--- /dev/null
+++ b/spec/presenters/tree_entry_presenter_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TreeEntryPresenter do
+ include Gitlab::Routing.url_helpers
+
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:tree) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.trees.first, repository) }
+ let(:presenter) { described_class.new(tree) }
+
+ describe '.web_url' do
+ it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/tree/#{tree.commit_id}/#{tree.path}") }
+ end
+end
diff --git a/spec/rack_servers/configs/puma.rb b/spec/rack_servers/configs/puma.rb
deleted file mode 100644
index d6b6d83d648..00000000000
--- a/spec/rack_servers/configs/puma.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-# Note: this file is used for testing puma in `spec/rack_servers/puma_spec.rb` only
-# Note: as per the convention in `config/puma.example.development.rb`,
-# this file will replace `/home/git` with the actual working directory
-
-directory '/home/git'
-threads 1, 10
-queue_requests false
-pidfile '/home/git/gitlab/tmp/pids/puma.pid'
-bind 'unix:///home/git/gitlab/tmp/tests/puma.socket'
-workers 1
-preload_app!
-worker_timeout 60
-
-require_relative "/home/git/gitlab/lib/gitlab/cluster/lifecycle_events"
-require_relative "/home/git/gitlab/lib/gitlab/cluster/puma_worker_killer_initializer"
-
-before_fork do
- Gitlab::Cluster::PumaWorkerKillerInitializer.start @config.options
- Gitlab::Cluster::LifecycleEvents.do_before_fork
-end
-
-Gitlab::Cluster::LifecycleEvents.set_puma_options @config.options
-on_worker_boot do
- Gitlab::Cluster::LifecycleEvents.do_worker_start
- File.write('/home/git/gitlab/tmp/tests/puma-worker-ready', Process.pid)
-end
-
-on_restart do
- Gitlab::Cluster::LifecycleEvents.do_master_restart
-end
diff --git a/spec/rack_servers/puma_spec.rb b/spec/rack_servers/puma_spec.rb
index 431fab87857..8290473821c 100644
--- a/spec/rack_servers/puma_spec.rb
+++ b/spec/rack_servers/puma_spec.rb
@@ -1,20 +1,20 @@
# frozen_string_literal: true
-require 'fileutils'
+require 'spec_helper'
+require 'fileutils'
require 'excon'
-require 'spec_helper'
-
describe 'Puma' do
before(:all) do
- project_root = File.expand_path('../..', __dir__)
-
- config_lines = File.read('spec/rack_servers/configs/puma.rb')
- .gsub('/home/git/gitlab', project_root)
- .gsub('/home/git', project_root)
-
- config_path = File.join(project_root, "tmp/tests/puma.rb")
+ project_root = Rails.root.to_s
+ config_lines = File.read(Rails.root.join('config/puma.example.development.rb'))
+ .gsub('config.ru', File.join(__dir__, 'configs/config.ru'))
+ .gsub('workers 2', 'workers 1')
+ .gsub('/home/git/gitlab.socket', File.join(project_root, 'tmp/tests/puma.socket'))
+ .gsub('on_worker_boot do', "on_worker_boot do\nFile.write('#{File.join(project_root, 'tmp/tests/puma-worker-ready')}', Process.pid)")
+ .gsub(%r{/home/git(/gitlab)?}, project_root)
+ config_path = File.join(project_root, 'tmp/tests/puma.rb')
@socket_path = File.join(project_root, 'tmp/tests/puma.socket')
File.write(config_path, config_lines)
@@ -44,11 +44,9 @@ describe 'Puma' do
end
after(:all) do
- begin
- WebMock.disable_net_connect!(allow_localhost: true)
- Process.kill('TERM', @puma_master_pid)
- rescue Errno::ESRCH
- end
+ WebMock.disable_net_connect!(allow_localhost: true)
+ Process.kill('TERM', @puma_master_pid)
+ rescue Errno::ESRCH
end
def wait_puma_boot!(master_pid, ready_file)
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
index 1271324a2ba..1dd0cb4817c 100644
--- a/spec/requests/api/badges_spec.rb
+++ b/spec/requests/api/badges_spec.rb
@@ -73,7 +73,7 @@ describe API::Badges do
let(:badge) { source.badges.first }
context "as a #{type}" do
- it 'returns 200' do
+ it 'returns 200', :quarantine do
user = public_send(type)
get api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user)
@@ -193,7 +193,7 @@ describe API::Badges do
end
context 'when authenticated as a maintainer/owner' do
- it 'updates the member' do
+ it 'updates the member', :quarantine do
put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer),
params: { link_url: example_url, image_url: example_url2 }
@@ -239,7 +239,7 @@ describe API::Badges do
end
end
- context 'when authenticated as a maintainer/owner' do
+ context 'when authenticated as a maintainer/owner', :quarantine do
it 'deletes the badge' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", maintainer)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index b38cd66986f..8b503777443 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -20,9 +20,9 @@ describe API::Branches do
let(:route) { "/projects/#{project_id}/repository/branches" }
shared_examples_for 'repository branches' do
- RSpec::Matchers.define :has_merged_branch_names_count do |expected|
+ RSpec::Matchers.define :has_up_to_merged_branch_names_count do |expected|
match do |actual|
- actual[:merged_branch_names].count == expected
+ expected >= actual[:merged_branch_names].count
end
end
@@ -36,10 +36,30 @@ describe API::Branches do
expect(branch_names).to match_array(project.repository.branch_names)
end
+ def check_merge_status(json_response)
+ merged, unmerged = json_response.partition { |branch| branch['merged'] }
+ merged_branches = merged.map { |branch| branch['name'] }
+ unmerged_branches = unmerged.map { |branch| branch['name'] }
+ expect(Set.new(merged_branches)).to eq(project.repository.merged_branch_names(merged_branches + unmerged_branches))
+ expect(project.repository.merged_branch_names(unmerged_branches)).to be_empty
+ end
+
it 'determines only a limited number of merged branch names' do
- expect(API::Entities::Branch).to receive(:represent).with(anything, has_merged_branch_names_count(2))
+ expect(API::Entities::Branch).to receive(:represent).with(anything, has_up_to_merged_branch_names_count(2)).and_call_original
get api(route, current_user), params: { per_page: 2 }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ check_merge_status(json_response)
+ end
+
+ it 'merge status matches reality on paginated input' do
+ get api(route, current_user), params: { per_page: 20, page: 2 }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ check_merge_status(json_response)
end
context 'when repository is disabled' do
diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb
deleted file mode 100644
index 6c7cb151c74..00000000000
--- a/spec/requests/api/circuit_breakers_spec.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-require 'spec_helper'
-
-describe API::CircuitBreakers do
- set(:user) { create(:user) }
- set(:admin) { create(:admin) }
-
- describe 'GET circuit_breakers/repository_storage' do
- it 'returns a 401 for anonymous users' do
- get api('/circuit_breakers/repository_storage')
-
- expect(response).to have_gitlab_http_status(401)
- end
-
- it 'returns a 403 for users' do
- get api('/circuit_breakers/repository_storage', user)
-
- expect(response).to have_gitlab_http_status(403)
- end
-
- it 'returns an Array of storages' do
- get api('/circuit_breakers/repository_storage', admin)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_kind_of(Array)
- expect(json_response).to be_empty
- end
-
- describe 'GET circuit_breakers/repository_storage/failing' do
- it 'returns an array of failing storages' do
- get api('/circuit_breakers/repository_storage/failing', admin)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_kind_of(Array)
- expect(json_response).to be_empty
- end
- end
- end
-
- describe 'DELETE circuit_breakers/repository_storage' do
- it 'clears all circuit_breakers' do
- delete api('/circuit_breakers/repository_storage', admin)
-
- expect(response).to have_gitlab_http_status(204)
- end
- end
-end
diff --git a/spec/requests/api/commit_statuses_spec.rb b/spec/requests/api/commit_statuses_spec.rb
index 9388343c392..b5e45f99109 100644
--- a/spec/requests/api/commit_statuses_spec.rb
+++ b/spec/requests/api/commit_statuses_spec.rb
@@ -306,7 +306,22 @@ describe API::CommitStatuses do
it 'responds with bad request status and validation errors' do
expect(response).to have_gitlab_http_status(400)
expect(json_response['message']['target_url'])
- .to include 'is blocked: Only allowed protocols are http, https'
+ .to include 'is blocked: Only allowed schemes are http, https'
+ end
+ end
+
+ context 'when target URL is an unsupported scheme' do
+ before do
+ post api(post_url, developer), params: {
+ state: 'pending',
+ target_url: 'git://example.com'
+ }
+ end
+
+ it 'responds with bad request status and validation errors' do
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['target_url'])
+ .to include 'is blocked: Only allowed schemes are http, https'
end
end
end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 066f1d6862a..f104da6ebba 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
require 'mime/types'
describe API::Commits do
+ include ProjectForksHelper
+
let(:user) { create(:user) }
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
@@ -317,6 +319,96 @@ describe API::Commits do
expect(response).to have_gitlab_http_status(201)
end
end
+
+ context 'when the API user is a guest' do
+ def last_commit_id(project, branch_name)
+ project.repository.find_branch(branch_name)&.dereferenced_target&.id
+ end
+
+ let(:public_project) { create(:project, :public, :repository) }
+ let!(:url) { "/projects/#{public_project.id}/repository/commits" }
+ let(:guest) { create(:user).tap { |u| public_project.add_guest(u) } }
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ context 'when start_project is provided' do
+ context 'when posting to a forked project the user owns' do
+ let!(:forked_project) { fork_project(public_project, guest, namespace: guest.namespace, repository: true) }
+ let!(:url) { "/projects/#{forked_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ end
+
+ context 'identified by Integer (id)' do
+ before do
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'adds a new commit to forked_project and returns a 201' do
+ expect { post api(url, guest), params: valid_c_params }
+ .to change { last_commit_id(forked_project, valid_c_params[:branch]) }
+ .and not_change { last_commit_id(public_project, valid_c_params[:start_branch]) }
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+
+ context 'identified by String (full_path)' do
+ before do
+ valid_c_params[:start_project] = public_project.full_path
+ end
+
+ it 'adds a new commit to forked_project and returns a 201' do
+ expect { post api(url, guest), params: valid_c_params }
+ .to change { last_commit_id(forked_project, valid_c_params[:branch]) }
+ .and not_change { last_commit_id(public_project, valid_c_params[:start_branch]) }
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+ end
+
+ context 'when the target project is not part of the fork network of start_project' do
+ let(:unrelated_project) { create(:project, :public, :repository, creator: guest) }
+ let!(:url) { "/projects/#{unrelated_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'when posting to a forked project the user does not have write access' do
+ let!(:forked_project) { fork_project(public_project, user, namespace: user.namespace, repository: true) }
+ let!(:url) { "/projects/#{forked_project.id}/repository/commits" }
+
+ before do
+ valid_c_params[:start_branch] = "master"
+ valid_c_params[:branch] = "patch"
+ valid_c_params[:start_project] = public_project.id
+ end
+
+ it 'returns a 403' do
+ post api(url, guest), params: valid_c_params
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
end
describe 'delete' do
@@ -1430,8 +1522,8 @@ describe API::Commits do
end
describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
- let!(:project) { create(:project, :repository, :private) }
- let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
+ let(:project) { create(:project, :repository, :private) }
+ let(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
let(:commit) { merged_mr.merge_request_diff.commits.last }
it 'returns the correct merge request' do
@@ -1456,6 +1548,17 @@ describe API::Commits do
expect(response).to have_gitlab_http_status(404)
end
+
+ context 'public project' do
+ let(:project) { create(:project, :repository, :public, :merge_requests_private) }
+ let(:non_member) { create(:user) }
+
+ it 'responds 403 when only members are allowed to read merge requests' do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", non_member)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
end
describe 'GET /projects/:id/repository/commits/:sha/signature' do
diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb
index 35c448d187d..ca1ffe3c524 100644
--- a/spec/requests/api/discussions_spec.rb
+++ b/spec/requests/api/discussions_spec.rb
@@ -13,7 +13,7 @@ describe API::Discussions do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) }
- it_behaves_like 'discussions API', 'projects', 'issues', 'iid' do
+ it_behaves_like 'discussions API', 'projects', 'issues', 'iid', can_reply_to_individual_notes: true do
let(:parent) { project }
let(:noteable) { issue }
let(:note) { issue_note }
@@ -37,7 +37,7 @@ describe API::Discussions do
let!(:diff_note) { create(:diff_note_on_merge_request, noteable: noteable, project: project, author: user) }
let(:parent) { project }
- it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid'
+ it_behaves_like 'discussions API', 'projects', 'merge_requests', 'iid', can_reply_to_individual_notes: true
it_behaves_like 'diff discussions API', 'projects', 'merge_requests', 'iid'
it_behaves_like 'resolvable discussions API', 'projects', 'merge_requests', 'iid'
end
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 493d3642255..8fc7fdc8632 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -32,6 +32,7 @@ describe API::Environments do
expect(json_response.first['name']).to eq(environment.name)
expect(json_response.first['external_url']).to eq(environment.external_url)
expect(json_response.first['project'].keys).to contain_exactly(*project_data_keys)
+ expect(json_response.first).not_to have_key("last_deployment")
end
end
@@ -188,4 +189,25 @@ describe API::Environments do
end
end
end
+
+ describe 'GET /projects/:id/environments/:environment_id' do
+ context 'as member of the project' do
+ it 'returns project environments' do
+ create(:deployment, :success, project: project, environment: environment)
+
+ get api("/projects/#{project.id}/environments/#{environment.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/environment')
+ end
+ end
+
+ context 'as non member' do
+ it 'returns a 404 status code' do
+ get api("/projects/#{project.id}/environments/#{environment.id}", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/events_spec.rb b/spec/requests/api/events_spec.rb
index 0ac23505de7..018691e8099 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -164,139 +164,4 @@ describe API::Events do
expect(json_response['message']).to eq('404 User Not Found')
end
end
-
- describe 'GET /projects/:id/events' do
- context 'when unauthenticated ' do
- it 'returns 404 for private project' do
- get api("/projects/#{private_project.id}/events")
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 200 status for a public project' do
- public_project = create(:project, :public)
-
- get api("/projects/#{public_project.id}/events")
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- context 'with inaccessible events' do
- let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
- let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
- let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
- let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
- let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: Event::CLOSED) }
-
- it 'returns only accessible events' do
- get api("/projects/#{public_project.id}/events", non_member)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.size).to eq(1)
- end
-
- it 'returns all events when the user has access' do
- get api("/projects/#{public_project.id}/events", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response.size).to eq(2)
- end
- end
-
- context 'pagination' do
- let(:public_project) { create(:project, :public) }
-
- before do
- create(:event,
- project: public_project,
- target: create(:issue, project: public_project, title: 'Issue 1'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-10'))
- create(:event,
- project: public_project,
- target: create(:issue, confidential: true, project: public_project, title: 'Confidential event'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-11'))
- create(:event,
- project: public_project,
- target: create(:issue, project: public_project, title: 'Issue 2'),
- action: Event::CLOSED,
- created_at: Date.parse('2018-12-12'))
- end
-
- it 'correctly returns the second page without inaccessible events' do
- get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 2 }
-
- titles = json_response.map { |event| event['target_title'] }
-
- expect(titles.first).to eq('Issue 1')
- expect(titles).not_to include('Confidential event')
- end
-
- it 'correctly returns the first page without inaccessible events' do
- get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 1 }
-
- titles = json_response.map { |event| event['target_title'] }
-
- expect(titles.first).to eq('Issue 2')
- expect(titles).not_to include('Confidential event')
- end
- end
-
- context 'when not permitted to read' do
- it 'returns 404' do
- get api("/projects/#{private_project.id}/events", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when authenticated' do
- it 'returns project events' do
- get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.size).to eq(1)
- end
-
- it 'returns 404 if project does not exist' do
- get api("/projects/1234/events", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- context 'when exists some events' do
- let(:merge_request1) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
- let(:merge_request2) { create(:merge_request, :closed, author: user, assignee: user, source_project: private_project, title: 'Test') }
-
- before do
- create_event(merge_request1)
- end
-
- it 'avoids N+1 queries' do
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
- end.count
-
- create_event(merge_request2)
-
- expect do
- get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
- end.not_to exceed_all_query_limit(control_count)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response.size).to eq(2)
- expect(json_response.map { |r| r['target_id'] }).to match_array([merge_request1.id, merge_request2.id])
- end
-
- def create_event(target)
- create(:event, project: private_project, author: user, target: target)
- end
- end
- end
end
diff --git a/spec/requests/api/graphql/gitlab_schema_spec.rb b/spec/requests/api/graphql/gitlab_schema_spec.rb
new file mode 100644
index 00000000000..28676bb02f4
--- /dev/null
+++ b/spec/requests/api/graphql/gitlab_schema_spec.rb
@@ -0,0 +1,150 @@
+require 'spec_helper'
+
+describe 'GitlabSchema configurations' do
+ include GraphqlHelpers
+
+ set(:project) { create(:project) }
+
+ shared_examples 'imposing query limits' do
+ describe '#max_complexity' do
+ context 'when complexity is too high' do
+ it 'shows an error' do
+ allow(GitlabSchema).to receive(:max_query_complexity).and_return 1
+
+ subject
+
+ expect(graphql_errors.flatten.first['message']).to include('which exceeds max complexity of 1')
+ end
+ end
+ end
+
+ describe '#max_depth' do
+ context 'when query depth is too high' do
+ it 'shows error' do
+ errors = { "message" => "Query has depth of 2, which exceeds max depth of 1" }
+ allow(GitlabSchema).to receive(:max_query_depth).and_return 1
+
+ subject
+
+ expect(graphql_errors.flatten).to include(errors)
+ end
+ end
+
+ context 'when query depth is within range' do
+ it 'has no error' do
+ allow(GitlabSchema).to receive(:max_query_depth).and_return 5
+
+ subject
+
+ expect(Array.wrap(graphql_errors).compact).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'regular queries' do
+ subject do
+ query = graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id name description))
+ post_graphql(query)
+ end
+
+ it_behaves_like 'imposing query limits'
+ end
+
+ context 'multiplexed queries' do
+ let(:current_user) { nil }
+
+ subject do
+ queries = [
+ { query: graphql_query_for('project', { 'fullPath' => '$fullPath' }, %w(id name description)) },
+ { query: graphql_query_for('echo', { 'text' => "$test" }, []), variables: { "test" => "Hello world" } },
+ { query: graphql_query_for('project', { 'fullPath' => project.full_path }, "userPermissions { createIssue }") }
+ ]
+
+ post_multiplex(queries, current_user: current_user)
+ end
+
+ it 'does not authenticate all queries' do
+ subject
+
+ expect(json_response.last['data']['project']).to be_nil
+ end
+
+ it_behaves_like 'imposing query limits' do
+ it "fails all queries when only one of the queries is too complex" do
+ # The `project` query above has a complexity of 5
+ allow(GitlabSchema).to receive(:max_query_complexity).and_return 4
+
+ subject
+
+ # Expect a response for each query, even though it will be empty
+ expect(json_response.size).to eq(3)
+ json_response.each do |single_query_response|
+ expect(single_query_response).not_to have_key('data')
+ end
+
+ # Expect errors for each query
+ expect(graphql_errors.size).to eq(3)
+ graphql_errors.each do |single_query_errors|
+ expect(single_query_errors.first['message']).to include('which exceeds max complexity of 4')
+ end
+ end
+ end
+
+ context 'authentication' do
+ let(:current_user) { project.owner }
+
+ it 'authenticates all queries' do
+ subject
+
+ expect(json_response.last['data']['project']['userPermissions']['createIssue']).to be(true)
+ end
+ end
+ end
+
+ context 'when IntrospectionQuery' do
+ it 'is not too complex' do
+ query = File.read(Rails.root.join('spec/fixtures/api/graphql/introspection.graphql'))
+
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_errors).to be_nil
+ end
+ end
+
+ context 'logging' 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: 0,
+ duration: 7
+ }
+
+ expect_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:duration).and_return(7)
+ expect(Gitlab::GraphqlLogger).to receive(:info).with(analyzer_memo)
+
+ post_graphql(query, current_user: nil)
+ end
+
+ it 'logs using `format_message`' do
+ expect_any_instance_of(Gitlab::GraphqlLogger).to receive(:format_message)
+
+ post_graphql(query, current_user: nil)
+ end
+ end
+
+ context "global id's" do
+ it 'uses GlobalID to expose ids' do
+ post_graphql(graphql_query_for('project', { 'fullPath' => project.full_path }, %w(id)),
+ current_user: project.owner)
+
+ parsed_id = GlobalID.parse(graphql_data['project']['id'])
+
+ expect(parsed_id).to eq(project.to_global_id)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/group_query_spec.rb b/spec/requests/api/graphql/group_query_spec.rb
new file mode 100644
index 00000000000..e0f1e4dbe9e
--- /dev/null
+++ b/spec/requests/api/graphql/group_query_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Based on spec/requests/api/groups_spec.rb
+# Should follow closely in order to ensure all situations are covered
+describe 'getting group information' do
+ include GraphqlHelpers
+ include UploadHelpers
+
+ let(:user1) { create(:user, can_create_group: false) }
+ let(:user2) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:public_group) { create(:group, :public) }
+ let(:private_group) { create(:group, :private) }
+
+ # similar to the API "GET /groups/:id"
+ describe "Query group(fullPath)" do
+ def group_query(group)
+ graphql_query_for('group', 'fullPath' => group.full_path)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(group_query(public_group))
+ end
+ end
+
+ context 'when unauthenticated' do
+ it 'returns nil for a private group' do
+ post_graphql(group_query(private_group))
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it 'returns a public group' do
+ post_graphql(group_query(public_group))
+
+ expect(graphql_data['group']).not_to be_nil
+ end
+ end
+
+ context "when authenticated as user" do
+ let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
+ let!(:group2) { create(:group, :private) }
+
+ before do
+ group1.add_owner(user1)
+ group2.add_owner(user2)
+ end
+
+ it "returns one of user1's groups" do
+ project = create(:project, namespace: group2, path: 'Foo')
+ create(:project_group_link, project: project, group: group1)
+
+ post_graphql(group_query(group1), current_user: user1)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(graphql_data['group']['id']).to eq(group1.to_global_id.to_s)
+ expect(graphql_data['group']['name']).to eq(group1.name)
+ expect(graphql_data['group']['path']).to eq(group1.path)
+ expect(graphql_data['group']['description']).to eq(group1.description)
+ expect(graphql_data['group']['visibility']).to eq(Gitlab::VisibilityLevel.string_level(group1.visibility_level))
+ expect(graphql_data['group']['avatarUrl']).to eq(group1.avatar_url(only_path: false))
+ expect(graphql_data['group']['webUrl']).to eq(group1.web_url)
+ expect(graphql_data['group']['requestAccessEnabled']).to eq(group1.request_access_enabled)
+ expect(graphql_data['group']['fullName']).to eq(group1.full_name)
+ expect(graphql_data['group']['fullPath']).to eq(group1.full_path)
+ expect(graphql_data['group']['parentId']).to eq(group1.parent_id)
+ end
+
+ it "does not return a non existing group" do
+ query = graphql_query_for('group', 'fullPath' => '1328')
+
+ post_graphql(query, current_user: user1)
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it "does not return a group not attached to user1" do
+ private_group.add_owner(user2)
+
+ post_graphql(group_query(private_group), current_user: user1)
+
+ expect(graphql_data['group']).to be_nil
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new do
+ post_graphql(group_query(group1), current_user: admin)
+ end.count
+
+ queries = [{ query: group_query(group1) },
+ { query: group_query(group2) }]
+
+ expect do
+ post_multiplex(queries, current_user: admin)
+ end.not_to exceed_query_limit(control_count)
+
+ expect(graphql_errors).to contain_exactly(nil, nil)
+ end
+ end
+
+ context "when authenticated as admin" do
+ it "returns any existing group" do
+ post_graphql(group_query(private_group), current_user: admin)
+
+ expect(graphql_data['group']['name']).to eq(private_group.name)
+ end
+
+ it "does not return a non existing group" do
+ query = graphql_query_for('group', 'fullPath' => '1328')
+ post_graphql(query, current_user: admin)
+
+ expect(graphql_data['group']).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/metadata_query_spec.rb b/spec/requests/api/graphql/metadata_query_spec.rb
new file mode 100644
index 00000000000..4c56c559cf9
--- /dev/null
+++ b/spec/requests/api/graphql/metadata_query_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting project information' do
+ include GraphqlHelpers
+
+ let(:query) { graphql_query_for('metadata', {}, all_graphql_fields_for('Metadata')) }
+
+ context 'logged in' do
+ it 'returns version and revision' do
+ post_graphql(query, current_user: create(:user))
+
+ expect(graphql_errors).to be_nil
+ expect(graphql_data).to eq(
+ 'metadata' => {
+ 'version' => Gitlab::VERSION,
+ 'revision' => Gitlab.revision
+ }
+ )
+ end
+ end
+
+ context 'anonymous user' do
+ it 'returns nothing' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_errors).to be_nil
+ expect(graphql_data).to eq('metadata' => nil)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/multiplexed_queries_spec.rb b/spec/requests/api/graphql/multiplexed_queries_spec.rb
new file mode 100644
index 00000000000..844fd979285
--- /dev/null
+++ b/spec/requests/api/graphql/multiplexed_queries_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'Multiplexed queries' do
+ include GraphqlHelpers
+
+ it 'returns responses for multiple queries' do
+ queries = [
+ { query: 'query($text: String) { echo(text: $text) }',
+ variables: { 'text' => 'Hello' } },
+ { query: 'query($text: String) { echo(text: $text) }',
+ variables: { 'text' => 'World' } }
+ ]
+
+ post_multiplex(queries)
+
+ first_response = json_response.first['data']['echo']
+ second_response = json_response.last['data']['echo']
+
+ expect(first_response).to eq('nil says: Hello')
+ expect(second_response).to eq('nil says: World')
+ end
+
+ it 'returns error and data combinations' do
+ queries = [
+ { query: 'query($text: String) { broken query }' },
+ { query: 'query working($text: String) { echo(text: $text) }',
+ variables: { 'text' => 'World' } }
+ ]
+
+ post_multiplex(queries)
+
+ first_response = json_response.first['errors']
+ second_response = json_response.last['data']['echo']
+
+ expect(first_response).not_to be_empty
+ expect(second_response).to eq('nil says: World')
+ end
+end
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
index 8f427d71a32..d75f0df9fd3 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_wip_spec.rb
@@ -11,7 +11,7 @@ describe 'Setting WIP status of a merge request' do
let(:mutation) do
variables = {
project_path: project.full_path,
- iid: merge_request.iid
+ iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_wip, variables.merge(input))
end
diff --git a/spec/requests/api/graphql/namespace/projects_spec.rb b/spec/requests/api/graphql/namespace/projects_spec.rb
new file mode 100644
index 00000000000..de1cd9586b6
--- /dev/null
+++ b/spec/requests/api/graphql/namespace/projects_spec.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'getting projects', :nested_groups do
+ include GraphqlHelpers
+
+ let(:group) { create(:group) }
+ let!(:project) { create(:project, namespace: subject) }
+ let(:nested_group) { create(:group, parent: group) }
+ let!(:nested_project) { create(:project, group: nested_group) }
+ let!(:public_project) { create(:project, :public, namespace: subject) }
+ let(:user) { create(:user) }
+ let(:include_subgroups) { true }
+
+ subject { group }
+
+ let(:query) do
+ graphql_query_for(
+ 'namespace',
+ { 'fullPath' => subject.full_path },
+ <<~QUERY
+ projects(includeSubgroups: #{include_subgroups}) {
+ edges {
+ node {
+ #{all_graphql_fields_for('Project')}
+ }
+ }
+ }
+ QUERY
+ )
+ end
+
+ before do
+ group.add_owner(user)
+ end
+
+ shared_examples 'a graphql namespace' do
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ it "includes the packages size if the user can read the statistics" do
+ post_graphql(query, current_user: user)
+
+ count = if include_subgroups
+ subject.all_projects.count
+ else
+ subject.projects.count
+ end
+
+ expect(graphql_data['namespace']['projects']['edges'].size).to eq(count)
+ end
+
+ context 'with no user' do
+ it 'finds only public projects' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['namespace']['projects']['edges'].size).to eq(1)
+ project = graphql_data['namespace']['projects']['edges'][0]['node']
+ expect(project['id']).to eq(public_project.to_global_id.to_s)
+ end
+ end
+ end
+
+ it_behaves_like 'a graphql namespace'
+
+ context 'when the namespace is a user' do
+ subject { user.namespace }
+ let(:include_subgroups) { false }
+
+ it_behaves_like 'a graphql namespace'
+ end
+
+ context 'when not including subgroups' do
+ let(:include_subgroups) { false }
+
+ it_behaves_like 'a graphql namespace'
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index c2934430821..4f9f916f22e 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -7,8 +7,8 @@ describe 'getting an issue list for a project' do
let(:current_user) { create(:user) }
let(:issues_data) { graphql_data['project']['issues']['edges'] }
let!(:issues) do
- create(:issue, project: project, discussion_locked: true)
- create(:issue, project: project)
+ [create(:issue, project: project, discussion_locked: true),
+ create(:issue, project: project)]
end
let(:fields) do
<<~QUERY
@@ -47,6 +47,30 @@ describe 'getting an issue list for a project' do
expect(issues_data[1]['node']['discussionLocked']).to eq true
end
+ context 'when limiting the number of results' do
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ "issues(first: 1) { #{fields} }"
+ )
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ it "is expected to check permissions on the first issue only" do
+ allow(Ability).to receive(:allowed?).and_call_original
+ # Newest first, we only want to see the newest checked
+ expect(Ability).not_to receive(:allowed?).with(current_user, :read_issue, issues.first)
+
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
context 'when the user does not have access to the issue' do
it 'returns nil' do
project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
diff --git a/spec/requests/api/graphql/project/merge_request_spec.rb b/spec/requests/api/graphql/project/merge_request_spec.rb
index deb6abbc026..74820d39102 100644
--- a/spec/requests/api/graphql/project/merge_request_spec.rb
+++ b/spec/requests/api/graphql/project/merge_request_spec.rb
@@ -70,13 +70,13 @@ describe 'getting merge request information nested in a project' do
context 'when there are pipelines' do
before do
- pipeline = create(
+ create(
:ci_pipeline,
project: merge_request.source_project,
ref: merge_request.source_branch,
sha: merge_request.diff_head_sha
)
- merge_request.update!(head_pipeline: pipeline)
+ merge_request.update_head_pipeline
end
it 'has a head pipeline' do
diff --git a/spec/requests/api/graphql/project/project_statistics_spec.rb b/spec/requests/api/graphql/project/project_statistics_spec.rb
new file mode 100644
index 00000000000..8683fa1f390
--- /dev/null
+++ b/spec/requests/api/graphql/project/project_statistics_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'rendering namespace statistics' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project) }
+ let!(:project_statistics) { create(:project_statistics, project: project, packages_size: 5.megabytes) }
+ let(:user) { create(:user) }
+
+ let(:query) do
+ graphql_query_for('project',
+ { 'fullPath' => project.full_path },
+ "statistics { #{all_graphql_fields_for('ProjectStatistics')} }")
+ end
+
+ before do
+ project.add_reporter(user)
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: user)
+ end
+ end
+
+ it "includes the packages size if the user can read the statistics" do
+ post_graphql(query, current_user: user)
+
+ expect(graphql_data['project']['statistics']['packagesSize']).to eq(5.megabytes)
+ end
+
+ context 'when the project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'includes the statistics regardless of the user' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['project']['statistics']).to be_present
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb
new file mode 100644
index 00000000000..67af612a4a0
--- /dev/null
+++ b/spec/requests/api/graphql/project/repository_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'getting a repository in a project' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { project.owner }
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('repository'.classify)}
+ QUERY
+ end
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('repository', {}, fields)
+ )
+ end
+
+ it 'returns repository' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['repository']).to be_present
+ end
+
+ context 'as a non-authorized user' do
+ let(:current_user) { create(:user) }
+
+ it 'returns nil' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']).to be(nil)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb
new file mode 100644
index 00000000000..b07aa1e12d3
--- /dev/null
+++ b/spec/requests/api/graphql/project/tree/tree_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'getting a tree in a project' do
+ include GraphqlHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:current_user) { project.owner }
+ let(:path) { "" }
+ let(:ref) { "master" }
+ let(:fields) do
+ <<~QUERY
+ tree(path:"#{path}", ref:"#{ref}") {
+ #{all_graphql_fields_for('tree'.classify)}
+ }
+ QUERY
+ end
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('repository', {}, fields)
+ )
+ end
+
+ context 'when path does not exist' do
+ let(:path) { "testing123" }
+
+ it 'returns empty tree' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['repository']['tree']['trees']['edges']).to eq([])
+ expect(graphql_data['project']['repository']['tree']['submodules']['edges']).to eq([])
+ expect(graphql_data['project']['repository']['tree']['blobs']['edges']).to eq([])
+ end
+ end
+
+ context 'when ref does not exist' do
+ let(:ref) { "testing123" }
+
+ it 'returns empty tree' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['repository']['tree']['trees']['edges']).to eq([])
+ expect(graphql_data['project']['repository']['tree']['submodules']['edges']).to eq([])
+ expect(graphql_data['project']['repository']['tree']['blobs']['edges']).to eq([])
+ end
+ end
+
+ context 'when ref and path exist' do
+ it 'returns tree' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['repository']['tree']).to be_present
+ end
+
+ it 'returns blobs, subtrees and submodules inside tree' do
+ post_graphql(query, current_user: current_user)
+
+ expect(graphql_data['project']['repository']['tree']['trees']['edges'].size).to be > 0
+ expect(graphql_data['project']['repository']['tree']['blobs']['edges'].size).to be > 0
+ expect(graphql_data['project']['repository']['tree']['submodules']['edges'].size).to be > 0
+ end
+ end
+
+ context 'when current user is nil' do
+ it 'returns empty project' do
+ post_graphql(query, current_user: nil)
+
+ expect(graphql_data['project']).to be(nil)
+ end
+ end
+end
diff --git a/spec/requests/api/graphql_spec.rb b/spec/requests/api/graphql_spec.rb
new file mode 100644
index 00000000000..656d6f8b50b
--- /dev/null
+++ b/spec/requests/api/graphql_spec.rb
@@ -0,0 +1,134 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'GraphQL' do
+ include GraphqlHelpers
+
+ let(:query) { graphql_query_for('echo', 'text' => 'Hello world' ) }
+
+ context 'graphql is disabled by feature flag' do
+ before do
+ stub_feature_flags(graphql: false)
+ end
+
+ it 'does not generate a route for GraphQL' do
+ expect { post_graphql(query) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+
+ context 'logging' do
+ shared_examples 'logging a graphql query' do
+ let(:expected_params) do
+ { query_string: query, variables: variables.to_s, duration: anything, depth: 1, complexity: 1 }
+ end
+
+ it 'logs a query with the expected params' do
+ expect(Gitlab::GraphqlLogger).to receive(:info).with(expected_params).once
+
+ post_graphql(query, variables: variables)
+ end
+
+ it 'does not instantiate any query analyzers' do # they are static and re-used
+ expect(GraphQL::Analysis::QueryComplexity).not_to receive(:new)
+ expect(GraphQL::Analysis::QueryDepth).not_to receive(:new)
+
+ 2.times { post_graphql(query, variables: variables) }
+ end
+ end
+
+ context 'with no variables' do
+ let(:variables) { {} }
+
+ it_behaves_like 'logging a graphql query'
+ end
+
+ context 'with variables' do
+ let(:variables) do
+ { "foo" => "bar" }
+ end
+
+ it_behaves_like 'logging a graphql query'
+ end
+
+ context 'when there is an error in the logger' do
+ before do
+ allow_any_instance_of(Gitlab::Graphql::QueryAnalyzers::LoggerAnalyzer).to receive(:process_variables).and_raise(StandardError.new("oh noes!"))
+ end
+
+ it 'logs the exception in Sentry and continues with the request' do
+ expect(Gitlab::Sentry).to receive(:track_exception).at_least(1).times
+ expect(Gitlab::GraphqlLogger).to receive(:info)
+
+ post_graphql(query, variables: {})
+ end
+ end
+ end
+
+ context 'invalid variables' do
+ it 'returns an error' do
+ post_graphql(query, variables: "This is not JSON")
+
+ expect(response).to have_gitlab_http_status(422)
+ expect(json_response['errors'].first['message']).not_to be_nil
+ end
+ end
+
+ context 'authentication', :allow_forgery_protection do
+ let(:user) { create(:user) }
+
+ it 'allows access to public data without authentication' do
+ post_graphql(query)
+
+ expect(graphql_data['echo']).to eq('nil says: Hello world')
+ end
+
+ it 'does not authenticate a user with an invalid CSRF' do
+ login_as(user)
+
+ post_graphql(query, headers: { 'X-CSRF-Token' => 'invalid' })
+
+ expect(graphql_data['echo']).to eq('nil says: Hello world')
+ end
+
+ it 'authenticates a user with a valid session token' do
+ # Create a session to get a CSRF token from
+ login_as(user)
+ get('/')
+
+ post '/api/graphql', params: { query: query }, headers: { 'X-CSRF-Token' => response.session['_csrf_token'] }
+
+ expect(graphql_data['echo']).to eq("\"#{user.username}\" says: Hello world")
+ end
+
+ context 'token authentication' do
+ let(:token) { create(:personal_access_token) }
+
+ before do
+ stub_authentication_activity_metrics(debug: false)
+ end
+
+ it 'Authenticates users with a PAT' do
+ expect(authentication_metrics)
+ .to increment(:user_authenticated_counter)
+ .and increment(:user_session_override_counter)
+ .and increment(:user_sessionless_authentication_counter)
+
+ post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
+
+ expect(graphql_data['echo']).to eq("\"#{token.user.username}\" says: Hello world")
+ end
+
+ context 'when the personal access token has no api scope' do
+ it 'does not log the user in' do
+ token.update(scopes: [:read_user])
+
+ post_graphql(query, headers: { 'PRIVATE-TOKEN' => token.token })
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(graphql_data['echo']).to eq('nil says: Hello world')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/group_variables_spec.rb b/spec/requests/api/group_variables_spec.rb
index e52f4c70407..d50bae3dc47 100644
--- a/spec/requests/api/group_variables_spec.rb
+++ b/spec/requests/api/group_variables_spec.rb
@@ -51,6 +51,7 @@ describe API::GroupVariables do
expect(response).to have_gitlab_http_status(200)
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
+ expect(json_response['variable_type']).to eq(variable.variable_type)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -87,24 +88,26 @@ describe API::GroupVariables do
it 'creates variable' do
expect do
- post api("/groups/#{group.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true }
+ post api("/groups/#{group.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true }
end.to change {group.variables.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
- expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['value']).to eq('PROTECTED_VALUE_2')
expect(json_response['protected']).to be_truthy
+ expect(json_response['variable_type']).to eq('env_var')
end
it 'creates variable with optional attributes' do
expect do
- post api("/groups/#{group.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ post api("/groups/#{group.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
end.to change {group.variables.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
+ expect(json_response['variable_type']).to eq('file')
end
it 'does not allow to duplicate variable key' do
@@ -145,7 +148,7 @@ describe API::GroupVariables do
initial_variable = group.variables.reload.first
value_before = initial_variable.value
- put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { value: 'VALUE_1_UP', protected: true }
+ put api("/groups/#{group.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true }
updated_variable = group.variables.reload.first
@@ -153,6 +156,7 @@ describe API::GroupVariables do
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
expect(updated_variable).to be_protected
+ expect(json_response['variable_type']).to eq('file')
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 7176bc23e34..c41408fba65 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -90,8 +90,9 @@ describe API::Groups do
it "includes statistics if requested" do
attributes = {
- storage_size: 702,
+ storage_size: 1158,
repository_size: 123,
+ wiki_size: 456,
lfs_objects_size: 234,
build_artifacts_size: 345
}.stringify_keys
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index a0c64d295c0..ed907841bd8 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -247,11 +247,10 @@ describe API::Helpers do
exception = RuntimeError.new('test error')
allow(exception).to receive(:backtrace).and_return(caller)
- expect(Raven).to receive(:capture_exception).with(exception, tags: {
- correlation_id: 'new-correlation-id'
- }, extra: {})
+ expect(Raven).to receive(:capture_exception).with(exception, tags:
+ a_hash_including(correlation_id: 'new-correlation-id'), extra: {})
- Gitlab::CorrelationId.use_id('new-correlation-id') do
+ Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
handle_api_exception(exception)
end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index cd85151ec1b..fcbff19bd61 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -26,6 +26,21 @@ describe API::Internal do
expect(json_response['redis']).to be(false)
end
+
+ context 'authenticating' do
+ it 'authenticates using a header' do
+ get api("/internal/check"),
+ headers: { API::Helpers::GITLAB_SHARED_SECRET_HEADER => Base64.encode64(secret_token) }
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'returns 401 when no credentials provided' do
+ get(api("/internal/check"))
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
end
describe 'GET /internal/broadcast_message' do
@@ -237,6 +252,14 @@ describe API::Internal do
expect(json_response['name']).to eq(user.name)
end
+
+ it 'responds successfully when a user is not found' do
+ get(api("/internal/discover"), params: { username: 'noone', secret_token: secret_token })
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(response.body).to eq('null')
+ end
end
describe "GET /internal/authorized_keys" do
@@ -298,7 +321,7 @@ describe API::Internal do
end
context 'with env passed as a JSON' do
- let(:gl_repository) { project.gl_repository(is_wiki: true) }
+ let(:gl_repository) { Gitlab::GlRepository::WIKI.identifier_for_subject(project) }
it 'sets env in RequestStore' do
obj_dir_relative = './objects'
@@ -324,7 +347,6 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_project_path"]).to eq(project.wiki.full_path)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user.reload.last_activity_on).to be_nil
@@ -337,7 +359,6 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_project_path"]).to eq(project.wiki.full_path)
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user.reload.last_activity_on).to eql(Date.today)
@@ -350,7 +371,6 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gitaly"]).not_to be_nil
@@ -370,7 +390,6 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gl_project_path"]).to eq(project.full_path)
expect(json_response["gitaly"]).not_to be_nil
@@ -479,6 +498,40 @@ describe API::Internal do
end
end
+ context "console message" do
+ before do
+ project.add_developer(user)
+ end
+
+ context "git pull" do
+ context "with no console message" do
+ it "has the correct payload" do
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['gl_console_messages']).to eq([])
+ end
+ end
+
+ context "with a console message" do
+ let(:console_messages) { ['message for the console'] }
+
+ it "has the correct payload" do
+ expect_next_instance_of(Gitlab::GitAccess) do |access|
+ expect(access).to receive(:check_for_console_messages)
+ .with('git-upload-pack')
+ .and_return(console_messages)
+ end
+
+ pull(key, project)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['gl_console_messages']).to eq(console_messages)
+ end
+ end
+ end
+ end
+
context "blocked user" do
let(:personal_project) { create(:project, namespace: user.namespace) }
@@ -591,6 +644,22 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(404)
expect(json_response["status"]).to be_falsey
end
+
+ it 'returns a 200 response when using a project path that does not exist' do
+ post(
+ api("/internal/allowed"),
+ params: {
+ key_id: key.id,
+ project: 'project/does-not-exist.git',
+ action: 'git-upload-pack',
+ secret_token: secret_token,
+ protocol: 'ssh'
+ }
+ )
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response["status"]).to be_falsey
+ end
end
context 'user does not exist' do
@@ -819,8 +888,10 @@ describe API::Internal do
}
end
+ let(:branch_name) { 'feature' }
+
let(:changes) do
- "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch"
+ "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{branch_name}"
end
let(:push_options) do
@@ -836,9 +907,9 @@ describe API::Internal do
it 'enqueues a PostReceive worker job' do
expect(PostReceive).to receive(:perform_async)
- .with(gl_repository, identifier, changes, push_options)
+ .with(gl_repository, identifier, changes, { ci: { skip: true } })
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
end
it 'decreases the reference counter and returns the result' do
@@ -846,17 +917,17 @@ describe API::Internal do
.and_return(reference_counter)
expect(reference_counter).to receive(:decrease).and_return(true)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['reference_counter_decreased']).to be(true)
end
it 'returns link to create new merge request' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['merge_request_urls']).to match [{
- "branch_name" => "new_branch",
- "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch",
+ "branch_name" => branch_name,
+ "url" => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{branch_name}",
"new_merge_request" => true
}]
end
@@ -864,16 +935,75 @@ describe API::Internal do
it 'returns empty array if printing_merge_request_link_enabled is false' do
project.update!(printing_merge_request_link_enabled: false)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(json_response['merge_request_urls']).to eq([])
end
+ it 'does not invoke MergeRequests::PushOptionsHandlerService' do
+ expect(MergeRequests::PushOptionsHandlerService).not_to receive(:new)
+
+ post api('/internal/post_receive'), params: valid_params
+ end
+
+ context 'when there are merge_request push options' do
+ before do
+ valid_params[:push_options] = ['merge_request.create']
+ end
+
+ it 'invokes MergeRequests::PushOptionsHandlerService' do
+ expect(MergeRequests::PushOptionsHandlerService).to receive(:new)
+
+ post api('/internal/post_receive'), params: valid_params
+ end
+
+ it 'creates a new merge request' do
+ expect do
+ Sidekiq::Testing.fake! do
+ post api('/internal/post_receive'), params: valid_params
+ end
+ end.to change { MergeRequest.count }.by(1)
+ end
+
+ it 'links to the newly created merge request' do
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['merge_request_urls']).to match [{
+ 'branch_name' => branch_name,
+ 'url' => "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/1",
+ 'new_merge_request' => false
+ }]
+ end
+
+ it 'adds errors on the service instance to warnings' do
+ expect_any_instance_of(
+ MergeRequests::PushOptionsHandlerService
+ ).to receive(:errors).at_least(:once).and_return(['my error'])
+
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error')
+ end
+
+ it 'adds ActiveRecord errors on invalid MergeRequest records to warnings' do
+ invalid_merge_request = MergeRequest.new
+ invalid_merge_request.errors.add(:base, 'my error')
+
+ expect_any_instance_of(
+ MergeRequests::CreateService
+ ).to receive(:execute).and_return(invalid_merge_request)
+
+ post api('/internal/post_receive'), params: valid_params
+
+ expect(json_response['warnings']).to eq('Error encountered with push options \'merge_request.create\': my error')
+ end
+ end
+
context 'broadcast message exists' do
let!(:broadcast_message) { create(:broadcast_message, starts_at: 1.day.ago, ends_at: 1.day.from_now ) }
it 'returns one broadcast message' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(broadcast_message.message)
@@ -882,7 +1012,7 @@ describe API::Internal do
context 'broadcast message does not exist' do
it 'returns empty string' do
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(nil)
@@ -893,7 +1023,7 @@ describe API::Internal do
it 'returns empty string' do
allow(BroadcastMessage).to receive(:current).and_return(nil)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response['broadcast_message']).to eq(nil)
@@ -905,7 +1035,7 @@ describe API::Internal do
project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'http', 'foo/baz')
project_moved.add_message
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response["redirected_message"]).to be_present
@@ -918,7 +1048,7 @@ describe API::Internal do
project_created = Gitlab::Checks::ProjectCreated.new(project, user, 'http')
project_created.add_message
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
expect(json_response["project_created_message"]).to be_present
@@ -930,7 +1060,7 @@ describe API::Internal do
it 'does not try to notify that project moved' do
allow_any_instance_of(Gitlab::Identifier).to receive(:identify).and_return(nil)
- post api("/internal/post_receive"), params: valid_params
+ post api('/internal/post_receive'), params: valid_params
expect(response).to have_gitlab_http_status(200)
end
@@ -956,9 +1086,9 @@ describe API::Internal do
def gl_repository_for(project_or_wiki)
case project_or_wiki
when ProjectWiki
- project_or_wiki.project.gl_repository(is_wiki: true)
+ Gitlab::GlRepository::WIKI.identifier_for_subject(project_or_wiki.project)
when Project
- project_or_wiki.gl_repository(is_wiki: false)
+ Gitlab::GlRepository::PROJECT.identifier_for_subject(project_or_wiki)
else
nil
end
diff --git a/spec/requests/api/issuable_bulk_update_spec.rb b/spec/requests/api/issuable_bulk_update_spec.rb
deleted file mode 100644
index 6463f3f5d35..00000000000
--- a/spec/requests/api/issuable_bulk_update_spec.rb
+++ /dev/null
@@ -1,154 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe API::IssuableBulkUpdate do
- set(:project) { create(:project) }
- set(:user) { project.creator }
-
- shared_examples "PUT /projects/:id/:issuable/bulk_update" do |issuable|
- def bulk_update(issuable, issuables, params, update_user = user)
- put api("/projects/#{project.id}/#{issuable.pluralize}/bulk_update", update_user),
- params: { issuable_ids: Array(issuables).map(&:id) }.merge(params)
- end
-
- context 'with not enough permissions' do
- it 'returns 403 for guest users' do
- guest = create(:user)
- project.add_guest(guest)
-
- bulk_update(issuable, issuables, { state_event: 'close' }, guest)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'when modifying the state' do
- it "closes #{issuable}" do
- bulk_update(issuable, issuables, { state_event: 'close' })
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['message']).to eq("#{issuables.count} #{issuable.pluralize(issuables.count)} updated")
- expect(project.public_send(issuable.pluralize).opened).to be_empty
- expect(project.public_send(issuable.pluralize).closed).not_to be_empty
- end
-
- it "opens #{issuable}" do
- closed_issuables = create_list("closed_#{issuable}".to_sym, 2)
-
- bulk_update(issuable, closed_issuables, { state_event: 'reopen' })
-
- expect(response).to have_gitlab_http_status(200)
- expect(project.public_send(issuable.pluralize).closed).to be_empty
- end
- end
-
- context 'when modifying the milestone' do
- let(:milestone) { create(:milestone, project: project) }
-
- it "adds a milestone #{issuable}" do
- bulk_update(issuable, issuables, { milestone_id: milestone.id })
-
- expect(response).to have_gitlab_http_status(200)
- issuables.each do |issuable|
- expect(issuable.reload.milestone).to eq(milestone)
- end
- end
-
- it 'removes a milestone' do
- issuables.first.milestone = milestone
- milestone_issuable = issuables.first
-
- bulk_update(issuable, [milestone_issuable], { milestone_id: 0 })
-
- expect(response).to have_gitlab_http_status(200)
- expect(milestone_issuable.reload.milestone).to eq(nil)
- end
- end
-
- context 'when modifying the subscription state' do
- it "subscribes to #{issuable}" do
- bulk_update(issuable, issuables, { subscription_event: 'subscribe' })
-
- expect(response).to have_gitlab_http_status(200)
- expect(issuables).to all(be_subscribed(user, project))
- end
-
- it 'unsubscribes from issues' do
- issuables.each do |issuable|
- issuable.subscriptions.create(user: user, project: project, subscribed: true)
- end
-
- bulk_update(issuable, issuables, { subscription_event: 'unsubscribe' })
-
- expect(response).to have_gitlab_http_status(200)
- issuables.each do |issuable|
- expect(issuable).not_to be_subscribed(user, project)
- end
- end
- end
-
- context 'when modifying the assignee' do
- it 'adds assignee to issues' do
- params = issuable == 'issue' ? { assignee_ids: [user.id] } : { assignee_id: user.id }
-
- bulk_update(issuable, issuables, params)
-
- expect(response).to have_gitlab_http_status(200)
- issuables.each do |issuable|
- expect(issuable.reload.assignees).to eq([user])
- end
- end
-
- it 'removes assignee' do
- assigned_issuable = issuables.first
-
- if issuable == 'issue'
- params = { assignee_ids: 0 }
- assigned_issuable.assignees << user
- else
- params = { assignee_id: 0 }
- assigned_issuable.update_attribute(:assignee, user)
- end
-
- bulk_update(issuable, [assigned_issuable], params)
- expect(assigned_issuable.reload.assignees).to eq([])
- end
- end
-
- context 'when modifying labels' do
- let(:bug) { create(:label, project: project) }
- let(:regression) { create(:label, project: project) }
- let(:feature) { create(:label, project: project) }
-
- it 'adds new labels' do
- bulk_update(issuable, issuables, { add_label_ids: [bug.id, regression.id, feature.id] })
-
- issuables.each do |issusable|
- expect(issusable.reload.label_ids).to contain_exactly(bug.id, regression.id, feature.id)
- end
- end
-
- it 'removes labels' do
- labled_issuable = issuables.first
- labled_issuable.labels << bug
- labled_issuable.labels << regression
- labled_issuable.labels << feature
-
- bulk_update(issuable, [labled_issuable], { remove_label_ids: [bug.id, regression.id] })
-
- expect(labled_issuable.reload.label_ids).to contain_exactly(feature.id)
- end
- end
- end
-
- it_behaves_like 'PUT /projects/:id/:issuable/bulk_update', 'issue' do
- let(:issuables) { create_list(:issue, 2, project: project) }
- end
-
- it_behaves_like 'PUT /projects/:id/:issuable/bulk_update', 'merge_request' do
- let(:merge_request_1) { create(:merge_request, source_project: project) }
- let(:merge_request_2) { create(:merge_request, :simple, source_project: project) }
- let(:issuables) { [merge_request_1, merge_request_2] }
- end
-end
diff --git a/spec/requests/api/issues/get_group_issues_spec.rb b/spec/requests/api/issues/get_group_issues_spec.rb
new file mode 100644
index 00000000000..8b02cf56e9f
--- /dev/null
+++ b/spec/requests/api/issues/get_group_issues_spec.rb
@@ -0,0 +1,652 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ describe 'GET /groups/:id/issues' do
+ let!(:group) { create(:group) }
+ let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
+ let!(:group_closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: group_project,
+ state: :closed,
+ milestone: group_milestone,
+ updated_at: 3.hours.ago,
+ created_at: 1.day.ago
+ end
+ let!(:group_confidential_issue) do
+ create :issue,
+ :confidential,
+ project: group_project,
+ author: author,
+ assignees: [assignee],
+ updated_at: 2.hours.ago,
+ created_at: 2.days.ago
+ end
+ let!(:group_issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description,
+ created_at: 5.days.ago
+ end
+ let!(:group_label) do
+ create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
+ end
+ let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
+ let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
+ let!(:group_empty_milestone) do
+ create(:milestone, title: '4.0.0', project: group_project)
+ end
+ let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
+
+ let(:base_url) { "/groups/#{group.id}/issues" }
+
+ shared_examples 'group issues statistics' do
+ it 'returns issues statistics' do
+ get api("/groups/#{group.id}/issues_statistics", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['statistics']).not_to be_nil
+ expect(json_response['statistics']['counts']['all']).to eq counts[:all]
+ expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
+ expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
+ end
+ end
+
+ context 'when group has subgroups', :nested_groups do
+ let(:subgroup_1) { create(:group, parent: group) }
+ let(:subgroup_2) { create(:group, parent: subgroup_1) }
+
+ let(:subgroup_1_project) { create(:project, :public, namespace: subgroup_1) }
+ let(:subgroup_2_project) { create(:project, namespace: subgroup_2) }
+
+ let!(:issue_1) { create(:issue, project: subgroup_1_project) }
+ let!(:issue_2) { create(:issue, project: subgroup_2_project) }
+
+ context 'when user is unauthenticated' do
+ it 'also returns subgroups public projects issues' do
+ get api(base_url)
+
+ expect_paginated_array_response([issue_1.id, group_closed_issue.id, group_issue.id])
+ end
+
+ it 'also returns subgroups public projects issues filtered by milestone' do
+ get api(base_url), params: { milestone: group_milestone.title }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: group_milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: group_milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+ end
+ end
+
+ context 'when user is a group member' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'also returns subgroups projects issues' do
+ get api(base_url, user)
+
+ expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'also returns subgroups public projects issues filtered by milestone' do
+ get api(base_url, user), params: { milestone: group_milestone.title }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 5, closed: 1, opened: 4 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 5, closed: 1, opened: 4 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 5, closed: 1, opened: 4 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 5, closed: 1, opened: 4 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: group_milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: group_milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+ end
+ end
+ end
+
+ context 'when user is unauthenticated' do
+ it 'lists all issues in public projects' do
+ get api(base_url)
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ it 'also returns subgroups public projects issues filtered by milestone' do
+ get api(base_url), params: { milestone: group_milestone.title }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: group_milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: group_milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+ end
+ end
+
+ context 'when user is a group member' do
+ before do
+ group_project.add_reporter(user)
+ end
+
+ it 'returns all group issues (including opened and closed)' do
+ get api(base_url, admin)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns group issues without confidential issues for non project members' do
+ get api(base_url, non_member), params: { state: :opened }
+
+ expect_paginated_array_response(group_issue.id)
+ end
+
+ it 'returns group confidential issues for author' do
+ get api(base_url, author), params: { state: :opened }
+
+ expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns group confidential issues for assignee' do
+ get api(base_url, assignee), params: { state: :opened }
+
+ expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns group issues with confidential issues for project members' do
+ get api(base_url, user), params: { state: :opened }
+
+ expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns group confidential issues for admin' do
+ get api(base_url, admin), params: { state: :opened }
+
+ expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
+ end
+
+ it 'returns only confidential issues' do
+ get api(base_url, user), params: { confidential: true }
+
+ expect_paginated_array_response(group_confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api(base_url, user), params: { confidential: false }
+
+ expect_paginated_array_response([group_closed_issue.id, group_issue.id])
+ end
+
+ it 'returns an array of labeled group issues' do
+ get api(base_url, user), params: { labels: group_label.title }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues with labels param as array' do
+ get api(base_url, user), params: { labels: [group_label.title] }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['labels']).to eq([group_label.title])
+ end
+
+ it 'returns an array of labeled group issues where all labels match' do
+ get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of labeled group issues where all labels match with labels param as array' do
+ get api(base_url, user), params: { labels: [group_label.title, 'foo', 'bar'] }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns issues matching given search string for title' do
+ get api(base_url, user), params: { search: group_issue.title }
+
+ expect_paginated_array_response(group_issue.id)
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api(base_url, user), params: { search: group_issue.description }
+
+ expect_paginated_array_response(group_issue.id)
+ end
+
+ context 'with labeled issues' do
+ let(:label_b) { create(:label, title: 'foo', project: group_project) }
+ let(:label_c) { create(:label, title: 'bar', project: group_project) }
+
+ before do
+ create(:label_link, label: label_b, target: group_issue)
+ create(:label_link, label: label_c, target: group_issue)
+
+ get api(base_url, user), params: params
+ end
+
+ let(:issue) { group_issue }
+ let(:label) { group_label }
+
+ it_behaves_like 'labeled issues with labels and label_name params'
+ end
+
+ it 'returns an array of issues found by iids' do
+ get api(base_url, user), params: { iids: [group_issue.iid] }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api(base_url, user), params: { iids: [0] }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if no group issue matches labels' do
+ get api(base_url, user), params: { labels: 'foo,bar' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of group issues with any label' do
+ get api(base_url, user), params: { labels: IssuesFinder::FILTER_ANY }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an array of group issues with any label with labels param as array' do
+ get api(base_url, user), params: { labels: [IssuesFinder::FILTER_ANY] }
+
+ expect_paginated_array_response(group_issue.id)
+ expect(json_response.first['id']).to eq(group_issue.id)
+ end
+
+ it 'returns an array of group issues with no label' do
+ get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE }
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
+ end
+
+ it 'returns an array of group issues with no label with labels param as array' do
+ get api(base_url, user), params: { labels: [IssuesFinder::FILTER_NONE] }
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api(base_url, user), params: { milestone: group_empty_milestone.title }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api(base_url, user), params: { milestone: 'foo' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api(base_url, user), params: { state: :opened, milestone: group_milestone.title }
+
+ expect_paginated_array_response(group_issue.id)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api(base_url, user), params: { milestone: group_milestone.title, state: :closed }
+
+ expect_paginated_array_response(group_closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get api(base_url, user), params: { milestone: no_milestone_title }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect_paginated_array_response(group_confidential_issue.id)
+ end
+
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api(base_url, user)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:group_issue2) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: group_project,
+ milestone: group_milestone,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description,
+ created_at: group_issue.created_at
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{base_url}?per_page=3", user)
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{base_url}?per_page=3&page=2", user)
+
+ expect_paginated_array_response([group_issue.id])
+ end
+ end
+ end
+
+ it 'sorts ascending when requested' do
+ get api("#{base_url}?sort=asc", user)
+
+ expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api("#{base_url}?order_by=updated_at", user)
+
+ group_issue.touch(:updated_at)
+
+ expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api(base_url, user), params: { order_by: :updated_at, sort: :asc }
+
+ expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: group_milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: group_milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: group_milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 3, closed: 1, opened: 2 } }
+
+ it_behaves_like 'group issues statistics'
+ end
+ end
+
+ context 'filtering by assignee_username' do
+ let(:another_assignee) { create(:assignee) }
+ let!(:issue1) { create(:issue, author: user2, project: group_project, created_at: 3.days.ago) }
+ let!(:issue2) { create(:issue, author: user2, project: group_project, created_at: 2.days.ago) }
+ let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: group_project, created_at: 1.day.ago) }
+
+ it 'returns issues with by assignee_username' do
+ get api(base_url, user), params: { assignee_username: [assignee.username], scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([issue3.id, group_confidential_issue.id])
+ end
+
+ it 'returns issues by assignee_username as string' do
+ get api(base_url, user), params: { assignee_username: assignee.username, scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([issue3.id, group_confidential_issue.id])
+ end
+
+ it 'returns error when multiple assignees are passed' do
+ get api(base_url, user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("allows one value, but found 2")
+ end
+
+ it 'returns error when assignee_username and assignee_id are passed together' do
+ get api(base_url, user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("mutually exclusive")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/issues/get_project_issues_spec.rb b/spec/requests/api/issues/get_project_issues_spec.rb
new file mode 100644
index 00000000000..0b0f754ab57
--- /dev/null
+++ b/spec/requests/api/issues/get_project_issues_spec.rb
@@ -0,0 +1,807 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignees: [assignee],
+ created_at: generate(:past_time),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+ set(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before(:all) do
+ project.add_reporter(user)
+ project.add_guest(guest)
+ end
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ shared_examples 'project issues statistics' do
+ it 'returns project issues statistics' do
+ get api("/issues_statistics", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['statistics']).not_to be_nil
+ expect(json_response['statistics']['counts']['all']).to eq counts[:all]
+ expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
+ expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
+ end
+ end
+
+ describe "GET /projects/:id/issues" do
+ let(:base_url) { "/projects/#{project.id}" }
+
+ context 'when unauthenticated' do
+ it 'returns public project issues' do
+ get api("/projects/#{project.id}/issues")
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ get api("/projects/#{project.id}/issues", user)
+
+ create_list(:issue, 3, project: project)
+
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{project.id}/issues", user)
+ end.count
+
+ expect do
+ get api("/projects/#{project.id}/issues", user)
+ end.not_to exceed_all_query_limit(control_count)
+ end
+
+ it 'returns 404 when project does not exist' do
+ max_project_id = Project.maximum(:id).to_i
+
+ get api("/projects/#{max_project_id + 1}/issues", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 on private projects for other users' do
+ private_project = create(:project, :private)
+ create(:issue, project: private_project)
+
+ get api("/projects/#{private_project.id}/issues", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns no issues when user has access to project but not issues' do
+ restricted_project = create(:project, :public, :issues_private)
+ create(:issue, project: restricted_project)
+
+ get api("/projects/#{restricted_project.id}/issues", non_member)
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns project issues without confidential issues for non project members' do
+ get api("#{base_url}/issues", non_member)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns project issues without confidential issues for project members with guest role' do
+ get api("#{base_url}/issues", guest)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns project confidential issues for author' do
+ get api("#{base_url}/issues", author)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns only confidential issues' do
+ get api("#{base_url}/issues", author), params: { confidential: true }
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api("#{base_url}/issues", author), params: { confidential: false }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns project confidential issues for assignee' do
+ get api("#{base_url}/issues", assignee)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns project issues with confidential issues for project members' do
+ get api("#{base_url}/issues", user)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns project confidential issues for admin' do
+ get api("#{base_url}/issues", admin)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of labeled project issues' do
+ get api("#{base_url}/issues", user), params: { labels: label.title }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of labeled project issues with labels param as array' do
+ get api("#{base_url}/issues", user), params: { labels: [label.title] }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ context 'with labeled issues' do
+ let(:label_b) { create(:label, title: 'foo', project: project) }
+ let(:label_c) { create(:label, title: 'bar', project: project) }
+
+ before do
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+
+ get api('/issues', user), params: params
+ end
+
+ it_behaves_like 'labeled issues with labels and label_name params'
+ end
+
+ it 'returns issues matching given search string for title' do
+ get api("#{base_url}/issues?search=#{issue.title}", user)
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api("#{base_url}/issues?search=#{issue.description}", user)
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of issues found by iids' do
+ get api("#{base_url}/issues", user), params: { iids: [issue.iid] }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api("#{base_url}/issues", user), params: { iids: [0] }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if not all labels matches' do
+ get api("#{base_url}/issues?labels=#{label.title},foo", user)
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of project issues with any label' do
+ get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_ANY }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of project issues with any label with labels param as array' do
+ get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_ANY] }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of project issues with no label' do
+ get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
+
+ expect_paginated_array_response([confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of project issues with no label with labels param as array' do
+ get api("#{base_url}/issues", user), params: { labels: [IssuesFinder::FILTER_NONE] }
+
+ expect_paginated_array_response([confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns an empty array if no project issue matches labels' do
+ get api("#{base_url}/issues", user), params: { labels: 'foo,bar' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: empty_milestone.title }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api("#{base_url}/issues", user), params: { milestone: :foo }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: milestone.title }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: milestone.title, state: :closed }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: no_milestone_title }
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns an array of issues with any milestone' do
+ get api("#{base_url}/issues", user), params: { milestone: any_milestone_title }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api("#{base_url}/issues", user)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:closed_issue2) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: closed_issue.created_at,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{base_url}/issues?per_page=3", user)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{base_url}/issues?per_page=3&page=2", user)
+
+ expect_paginated_array_response([closed_issue.id])
+ end
+ end
+ end
+
+ it 'sorts ascending when requested' do
+ get api("#{base_url}/issues", user), params: { sort: :asc }
+
+ expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api("#{base_url}/issues", user), params: { order_by: :updated_at }
+
+ issue.touch(:updated_at)
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api("#{base_url}/issues", user), params: { order_by: :updated_at, sort: :asc }
+
+ expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'project issues statistics'
+ end
+ end
+
+ context 'filtering by assignee_username' do
+ let(:another_assignee) { create(:assignee) }
+ let!(:issue1) { create(:issue, author: user2, project: project, created_at: 3.days.ago) }
+ let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
+ let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
+
+ it 'returns issues by assignee_username' do
+ get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([confidential_issue.id, issue3.id])
+ end
+
+ it 'returns issues by assignee_username as string' do
+ get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([confidential_issue.id, issue3.id])
+ end
+
+ it 'returns error when multiple assignees are passed' do
+ get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("allows one value, but found 2")
+ end
+
+ it 'returns error when assignee_username and assignee_id are passed together' do
+ get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("mutually exclusive")
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/issues/:issue_iid' do
+ context 'when unauthenticated' do
+ it 'returns public issues' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}")
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ it 'exposes known attributes' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(issue.id)
+ expect(json_response['iid']).to eq(issue.iid)
+ expect(json_response['project_id']).to eq(issue.project.id)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['description']).to eq(issue.description)
+ expect(json_response['state']).to eq(issue.state)
+ expect(json_response['closed_at']).to be_falsy
+ expect(json_response['created_at']).to be_present
+ expect(json_response['updated_at']).to be_present
+ expect(json_response['labels']).to eq(issue.label_names)
+ expect(json_response['milestone']).to be_a Hash
+ expect(json_response['assignees']).to be_a Array
+ expect(json_response['assignee']).to be_a Hash
+ expect(json_response['author']).to be_a Hash
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'exposes the closed_at attribute' do
+ get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['closed_at']).to be_present
+ end
+
+ context 'links exposure' do
+ it 'exposes related resources full URIs' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
+
+ links = json_response['_links']
+
+ expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
+ expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
+ expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
+ expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
+ end
+ end
+
+ it 'returns a project issue by internal id' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(issue.title)
+ expect(json_response['iid']).to eq(issue.iid)
+ end
+
+ it 'returns 404 if issue id not found' do
+ get api("/projects/#{project.id}/issues/54321", user)
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the issue ID is used' do
+ get api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ context 'confidential issues' do
+ it 'returns 404 for non project members' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 for project members with guest role' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns confidential issue for project members' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it 'returns confidential issue for author' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it 'returns confidential issue for assignee' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+
+ it 'returns confidential issue for admin' do
+ get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq(confidential_issue.title)
+ expect(json_response['iid']).to eq(confidential_issue.iid)
+ end
+ end
+ end
+
+ describe 'GET :id/issues/:issue_iid/closed_by' do
+ let(:merge_request) do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "closes #{issue.to_reference}")
+ end
+
+ before do
+ create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
+ end
+
+ context 'when unauthenticated' do
+ it 'return public project issues' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
+
+ expect_paginated_array_response(merge_request.id)
+ end
+ end
+
+ it 'returns merge requests that will close issue on merge' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
+
+ expect_paginated_array_response(merge_request.id)
+ end
+
+ context 'when no merge requests will close issue' do
+ it 'returns empty array' do
+ get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user)
+
+ expect_paginated_array_response([])
+ end
+ end
+
+ it "returns 404 when issue doesn't exists" do
+ get api("/projects/#{project.id}/issues/0/closed_by", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe 'GET :id/issues/:issue_iid/related_merge_requests' do
+ def get_related_merge_requests(project_id, issue_iid, user = nil)
+ get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
+ end
+
+ def create_referencing_mr(user, project, issue)
+ attributes = {
+ author: user,
+ source_project: project,
+ target_project: project,
+ source_branch: 'master',
+ target_branch: 'test',
+ description: "See #{issue.to_reference}"
+ }
+ create(:merge_request, attributes).tap do |merge_request|
+ create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
+ end
+ end
+
+ let!(:related_mr) { create_referencing_mr(user, project, issue) }
+
+ context 'when unauthenticated' do
+ it 'return list of referenced merge requests from issue' do
+ get_related_merge_requests(project.id, issue.iid)
+
+ expect_paginated_array_response(related_mr.id)
+ end
+
+ it 'renders 404 if project is not visible' do
+ private_project = create(:project, :private)
+ private_issue = create(:issue, project: private_project)
+ create_referencing_mr(user, private_project, private_issue)
+
+ get_related_merge_requests(private_project.id, private_issue.iid)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ it 'returns merge requests that mentioned a issue' do
+ create(:merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: 'Some description')
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response(related_mr.id)
+ end
+
+ it 'returns merge requests cross-project wide' do
+ project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ merge_request = create_referencing_mr(user, project2, issue)
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response([related_mr.id, merge_request.id])
+ end
+
+ it 'does not generate references to projects with no access' do
+ private_project = create(:project, :private)
+ create_referencing_mr(private_project.creator, private_project, issue)
+
+ get_related_merge_requests(project.id, issue.iid, user)
+
+ expect_paginated_array_response(related_mr.id)
+ end
+
+ context 'no merge request mentioned a issue' do
+ it 'returns empty array' do
+ get_related_merge_requests(project.id, closed_issue.iid, user)
+
+ expect_paginated_array_response([])
+ end
+ end
+
+ it "returns 404 when issue doesn't exists" do
+ get_related_merge_requests(project.id, 0, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/issues/:issue_iid/user_agent_detail' do
+ let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
+
+ context 'when unauthenticated' do
+ it 'returns unauthorized' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
+
+ expect(response).to have_gitlab_http_status(401)
+ end
+ end
+
+ it 'exposes known attributes' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
+ expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
+ expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
+ end
+
+ it 'returns unauthorized for non-admin users' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ describe 'GET projects/:id/issues/:issue_iid/participants' do
+ it_behaves_like 'issuable participants endpoint' do
+ let(:entity) { issue }
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb
new file mode 100644
index 00000000000..9b9cc778fb3
--- /dev/null
+++ b/spec/requests/api/issues/issues_spec.rb
@@ -0,0 +1,796 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignees: [assignee],
+ created_at: generate(:past_time),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+ set(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before(:all) do
+ project.add_reporter(user)
+ project.add_guest(guest)
+ end
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ shared_examples 'issues statistics' do
+ it 'returns issues statistics' do
+ get api("/issues_statistics", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['statistics']).not_to be_nil
+ expect(json_response['statistics']['counts']['all']).to eq counts[:all]
+ expect(json_response['statistics']['counts']['closed']).to eq counts[:closed]
+ expect(json_response['statistics']['counts']['opened']).to eq counts[:opened]
+ end
+ end
+
+ describe 'GET /issues' do
+ context 'when unauthenticated' do
+ it 'returns an array of all issues' do
+ get api('/issues'), params: { scope: 'all' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ end
+
+ it 'returns authentication error without any scope' do
+ get api('/issues')
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns authentication error when scope is assigned-to-me' do
+ get api('/issues'), params: { scope: 'assigned-to-me' }
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns authentication error when scope is created-by-me' do
+ get api('/issues'), params: { scope: 'created-by-me' }
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api('/issues'), params: { milestone: 'foo', scope: 'all' }
+
+ expect(response).to have_http_status(200)
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api('/issues'), params: { milestone: milestone.title, scope: 'all' }
+
+ expect(response).to have_http_status(200)
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'issues_statistics' do
+ it 'returns authentication error without any scope' do
+ get api('/issues_statistics')
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns authentication error when scope is assigned_to_me' do
+ get api('/issues_statistics'), params: { scope: 'assigned_to_me' }
+
+ expect(response).to have_http_status(401)
+ end
+
+ it 'returns authentication error when scope is created_by_me' do
+ get api('/issues_statistics'), params: { scope: 'created_by_me' }
+
+ expect(response).to have_http_status(401)
+ end
+
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns an array of issues' do
+ get api('/issues', user)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.last).to have_key('web_url')
+ end
+
+ it 'returns an array of closed issues' do
+ get api('/issues', user), params: { state: :closed }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of opened issues' do
+ get api('/issues', user), params: { state: :opened }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of all issues' do
+ get api('/issues', user), params: { state: :all }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns issues assigned to me' do
+ issue2 = create(:issue, assignees: [user2], project: project)
+
+ get api('/issues', user2), params: { scope: 'assigned_to_me' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues assigned to me (kebab-case)' do
+ issue2 = create(:issue, assignees: [user2], project: project)
+
+ get api('/issues', user2), params: { scope: 'assigned-to-me' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues authored by the given author id' do
+ issue2 = create(:issue, author: user2, project: project)
+
+ get api('/issues', user), params: { author_id: user2.id, scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues assigned to the given assignee id' do
+ issue2 = create(:issue, assignees: [user2], project: project)
+
+ get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues authored by the given author id and assigned to the given assignee id' do
+ issue2 = create(:issue, author: user2, assignees: [user2], project: project)
+
+ get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues with no assignee' do
+ issue2 = create(:issue, author: user2, project: project)
+
+ get api('/issues', user), params: { assignee_id: 0, scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues with no assignee' do
+ issue2 = create(:issue, author: user2, project: project)
+
+ get api('/issues', user), params: { assignee_id: 'None', scope: 'all' }
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues with any assignee' do
+ # This issue without assignee should not be returned
+ create(:issue, author: user2, project: project)
+
+ get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' }
+
+ expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
+ end
+
+ it 'returns only confidential issues' do
+ get api('/issues', user), params: { confidential: true, scope: 'all' }
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns only public issues' do
+ get api('/issues', user), params: { confidential: false }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns issues reacted by the authenticated user' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+ create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
+
+ get api('/issues', user2), params: { my_reaction_emoji: 'Any', scope: 'all' }
+
+ expect_paginated_array_response([issue2.id, issue.id])
+ end
+
+ it 'returns issues not reacted by the authenticated user' do
+ issue2 = create(:issue, project: project, author: user, assignees: [user])
+ create(:award_emoji, awardable: issue2, user: user2, name: 'star')
+
+ get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' }
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns issues matching given search string for title' do
+ get api('/issues', user), params: { search: issue.title }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns issues matching given search string for title and scoped in title' do
+ get api('/issues', user), params: { search: issue.title, in: 'title' }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an empty array if no issue matches given search string for title and scoped in description' do
+ get api('/issues', user), params: { search: issue.title, in: 'description' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns issues matching given search string for description' do
+ get api('/issues', user), params: { search: issue.description }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ context 'filtering before a specific date' do
+ let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) }
+
+ it 'returns issues created before a specific date' do
+ get api('/issues?created_before=2000-01-02T00:00:00.060Z', user)
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues updated before a specific date' do
+ get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user)
+
+ expect_paginated_array_response(issue2.id)
+ end
+ end
+
+ context 'filtering after a specific date' do
+ let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) }
+
+ it 'returns issues created after a specific date' do
+ get api("/issues?created_after=#{issue2.created_at}", user)
+
+ expect_paginated_array_response(issue2.id)
+ end
+
+ it 'returns issues updated after a specific date' do
+ get api("/issues?updated_after=#{issue2.updated_at}", user)
+
+ expect_paginated_array_response(issue2.id)
+ end
+ end
+
+ context 'filter by labels or label_name param' do
+ context 'N+1' do
+ let(:label_b) { create(:label, title: 'foo', project: project) }
+ let(:label_c) { create(:label, title: 'bar', project: project) }
+
+ before do
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+ end
+ it 'tests N+1' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
+ end
+
+ label_d = create(:label, title: 'dar', project: project)
+ label_e = create(:label, title: 'ear', project: project)
+ create(:label_link, label: label_d, target: issue)
+ create(:label_link, label: label_e, target: issue)
+
+ expect do
+ get api('/issues', user), params: { labels: [label.title, label_b.title, label_c.title] }
+ end.not_to exceed_query_limit(control)
+ expect(issue.labels.count).to eq(5)
+ end
+ end
+
+ it 'returns an array of labeled issues' do
+ get api('/issues', user), params: { labels: label.title }
+
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ it 'returns an array of labeled issues with labels param as array' do
+ get api('/issues', user), params: { labels: [label.title] }
+
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label.title])
+ end
+
+ context 'with labeled issues' do
+ let(:label_b) { create(:label, title: 'foo', project: project) }
+ let(:label_c) { create(:label, title: 'bar', project: project) }
+
+ before do
+ create(:label_link, label: label_b, target: issue)
+ create(:label_link, label: label_c, target: issue)
+
+ get api('/issues', user), params: params
+ end
+
+ it_behaves_like 'labeled issues with labels and label_name params'
+ end
+
+ it 'returns an empty array if no issue matches labels' do
+ get api('/issues', user), params: { labels: 'foo,bar' }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if no issue matches labels with labels param as array' do
+ get api('/issues', user), params: { labels: %w(foo bar) }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of labeled issues matching given state' do
+ get api('/issues', user), params: { labels: label.title, state: :opened }
+
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label.title])
+ expect(json_response.first['state']).to eq('opened')
+ end
+
+ it 'returns an array of labeled issues matching given state with labels param as array' do
+ get api('/issues', user), params: { labels: [label.title], state: :opened }
+
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label.title])
+ expect(json_response.first['state']).to eq('opened')
+ end
+
+ it 'returns an empty array if no issue matches labels and state filters' do
+ get api('/issues', user), params: { labels: label.title, state: :closed }
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues with any label' do
+ get api('/issues', user), params: { labels: IssuesFinder::FILTER_ANY }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of issues with any label with labels param as array' do
+ get api('/issues', user), params: { labels: [IssuesFinder::FILTER_ANY] }
+
+ expect_paginated_array_response(issue.id)
+ end
+
+ it 'returns an array of issues with no label' do
+ get api('/issues', user), params: { labels: IssuesFinder::FILTER_NONE }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no label with labels param as array' do
+ get api('/issues', user), params: { labels: [IssuesFinder::FILTER_NONE] }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no label when using the legacy No+Label filter' do
+ get api('/issues', user), params: { labels: 'No Label' }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no label when using the legacy No+Label filter with labels param as array' do
+ get api('/issues', user), params: { labels: ['No Label'] }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api("/issues?milestone=#{empty_milestone.title}", user)
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api('/issues?milestone=foo', user)
+
+ expect_paginated_array_response([])
+ end
+
+ it 'returns an array of issues in given milestone' do
+ get api("/issues?milestone=#{milestone.title}", user)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of issues in given milestone_title param' do
+ get api("/issues?milestone_title=#{milestone.title}", user)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'returns an array of issues matching state in milestone' do
+ get api("/issues?milestone=#{milestone.title}&state=closed", user)
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone' do
+ get api("/issues?milestone=#{no_milestone_title}", author)
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns an array of issues with no milestone using milestone_title param' do
+ get api("/issues?milestone_title=#{no_milestone_title}", author)
+
+ expect_paginated_array_response(confidential_issue.id)
+ end
+
+ it 'returns an array of issues found by iids' do
+ get api('/issues', user), params: { iids: [closed_issue.iid] }
+
+ expect_paginated_array_response(closed_issue.id)
+ end
+
+ it 'returns an empty array if iid does not exist' do
+ get api('/issues', user), params: { iids: [0] }
+
+ expect_paginated_array_response([])
+ end
+
+ context 'without sort params' do
+ it 'sorts by created_at descending by default' do
+ get api('/issues', user)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ context 'with 2 issues with same created_at' do
+ let!(:closed_issue2) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: closed_issue.created_at,
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+
+ it 'page breaks first page correctly' do
+ get api('/issues?per_page=2', user)
+
+ expect_paginated_array_response([issue.id, closed_issue2.id])
+ end
+
+ it 'page breaks second page correctly' do
+ get api('/issues?per_page=2&page=2', user)
+
+ expect_paginated_array_response([closed_issue.id])
+ end
+ end
+ end
+
+ it 'sorts ascending when requested' do
+ get api('/issues?sort=asc', user)
+
+ expect_paginated_array_response([closed_issue.id, issue.id])
+ end
+
+ it 'sorts by updated_at descending when requested' do
+ get api('/issues?order_by=updated_at', user)
+
+ issue.touch(:updated_at)
+
+ expect_paginated_array_response([issue.id, closed_issue.id])
+ end
+
+ it 'sorts by updated_at ascending when requested' do
+ get api('/issues?order_by=updated_at&sort=asc', user)
+
+ issue.touch(:updated_at)
+
+ expect_paginated_array_response([closed_issue.id, issue.id])
+ end
+
+ it 'matches V4 response schema' do
+ get api('/issues', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ end
+
+ it 'returns a related merge request count of 0 if there are no related merge requests' do
+ get api('/issues', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ expect(json_response.first).to include('merge_requests_count' => 0)
+ end
+
+ it 'returns a related merge request count > 0 if there are related merge requests' do
+ create(:merge_requests_closing_issues, issue: issue)
+
+ get api('/issues', user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/issues')
+ expect(json_response.first).to include('merge_requests_count' => 1)
+ end
+
+ context 'issues_statistics' do
+ context 'no state is treated as all state' do
+ let(:params) { {} }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'statistics when all state is passed' do
+ let(:params) { { state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'closed state is treated as all state' do
+ let(:params) { { state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'opened state is treated as all state' do
+ let(:params) { { state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and no state treated as all state' do
+ let(:params) { { milestone: milestone.title } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and all state' do
+ let(:params) { { milestone: milestone.title, state: :all } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and closed state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :closed } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'when filtering by milestone and opened state treated as all state' do
+ let(:params) { { milestone: milestone.title, state: :opened } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+
+ context 'sort does not affect statistics ' do
+ let(:params) { { state: :opened, order_by: 'updated_at' } }
+ let(:counts) { { all: 2, closed: 1, opened: 1 } }
+
+ it_behaves_like 'issues statistics'
+ end
+ end
+
+ context 'filtering by assignee_username' do
+ let(:another_assignee) { create(:assignee) }
+ let!(:issue1) { create(:issue, author: user2, project: project, created_at: 3.days.ago) }
+ let!(:issue2) { create(:issue, author: user2, project: project, created_at: 2.days.ago) }
+ let!(:issue3) { create(:issue, author: user2, assignees: [assignee, another_assignee], project: project, created_at: 1.day.ago) }
+
+ it 'returns issues with by assignee_username' do
+ get api("/issues", user), params: { assignee_username: [assignee.username], scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([confidential_issue.id, issue3.id])
+ end
+
+ it 'returns issues by assignee_username as string' do
+ get api("/issues", user), params: { assignee_username: assignee.username, scope: 'all' }
+
+ expect(issue3.reload.assignees.pluck(:id)).to match_array([assignee.id, another_assignee.id])
+ expect_paginated_array_response([confidential_issue.id, issue3.id])
+ end
+
+ it 'returns error when multiple assignees are passed' do
+ get api("/issues", user), params: { assignee_username: [assignee.username, another_assignee.username], scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("allows one value, but found 2")
+ end
+
+ it 'returns error when assignee_username and assignee_id are passed together' do
+ get api("/issues", user), params: { assignee_username: [assignee.username], assignee_id: another_assignee.id, scope: 'all' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response["error"]).to include("mutually exclusive")
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/issues/:issue_iid' do
+ it 'rejects a non member from deleting an issue' do
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'rejects a developer from deleting an issue' do
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ context 'when the user is project owner' do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ it 'deletes the issue if an admin requests it' do
+ delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) }
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ delete api("/projects/#{project.id}/issues/123", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ it 'returns 404 when using the issue ID instead of IID' do
+ delete api("/projects/#{project.id}/issues/#{issue.id}", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe 'time tracking endpoints' do
+ let(:issuable) { issue }
+
+ include_examples 'time tracking endpoints', 'issue'
+ end
+end
diff --git a/spec/requests/api/issues/post_projects_issues_spec.rb b/spec/requests/api/issues/post_projects_issues_spec.rb
new file mode 100644
index 00000000000..b74e8867310
--- /dev/null
+++ b/spec/requests/api/issues/post_projects_issues_spec.rb
@@ -0,0 +1,549 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignees: [assignee],
+ created_at: generate(:past_time),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+ set(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before(:all) do
+ project.add_reporter(user)
+ project.add_guest(guest)
+ end
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ describe 'POST /projects/:id/issues' do
+ context 'support for deprecated assignee_id' do
+ it 'creates a new project issue' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', assignee_id: user2.id }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new project issue when assignee_id is empty' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', assignee_id: '' }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['assignee']).to be_nil
+ end
+ end
+
+ context 'single assignee restrictions' do
+ it 'creates a new project issue with no more than one assignee' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', assignee_ids: [user2.id, guest.id] }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['assignees'].count).to eq(1)
+ end
+ end
+
+ context 'user does not have permissions to create issue' do
+ let(:not_member) { create(:user) }
+
+ before do
+ project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'renders 403' do
+ post api("/projects/#{project.id}/issues", not_member), params: { title: 'new issue' }
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'an internal ID is provided' do
+ context 'by an admin' do
+ it 'sets the internal ID on the new issue' do
+ post api("/projects/#{project.id}/issues", admin),
+ params: { title: 'new issue', iid: 9001 }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['iid']).to eq 9001
+ end
+ end
+
+ context 'by an owner' do
+ it 'sets the internal ID on the new issue' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', iid: 9001 }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['iid']).to eq 9001
+ end
+ end
+
+ context 'by a group owner' do
+ let(:group) { create(:group) }
+ let(:group_project) { create(:project, :public, namespace: group) }
+
+ it 'sets the internal ID on the new issue' do
+ group.add_owner(user2)
+ post api("/projects/#{group_project.id}/issues", user2),
+ params: { title: 'new issue', iid: 9001 }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['iid']).to eq 9001
+ end
+ end
+
+ context 'by another user' do
+ it 'ignores the given internal ID' do
+ post api("/projects/#{project.id}/issues", user2),
+ params: { title: 'new issue', iid: 9001 }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['iid']).not_to eq 9001
+ end
+ end
+ end
+
+ it 'creates a new project issue' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['confidential']).to be_falsy
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new project issue with labels param as array' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', labels: %w(label label2), weight: 3, assignee_ids: [user2.id] }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['confidential']).to be_falsy
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new confidential project issue' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', confidential: true }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a new confidential project issue with a different param' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', confidential: 'y' }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'creates a public issue when confidential param is false' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', confidential: false }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'creates a public issue when confidential param is invalid' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', confidential: 'foo' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+
+ it 'returns a 400 bad request if title not given' do
+ post api("/projects/#{project.id}/issues", user), params: { labels: 'label, label2' }
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'allows special label names' do
+ post api("/projects/#{project.id}/issues", user),
+ params: {
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+ }
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'allows special label names with labels param as array' do
+ post api("/projects/#{project.id}/issues", user),
+ params: {
+ title: 'new issue',
+ labels: ['label', 'label?', 'label&foo, ?, &']
+ }
+ expect(response.status).to eq(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'g' * 256 }
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+
+ context 'resolving discussions' do
+ let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
+ let(:merge_request) { discussion.noteable }
+ let(:project) { merge_request.source_project }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'resolving all discussions in a merge request' do
+ before do
+ post api("/projects/#{project.id}/issues", user),
+ params: {
+ title: 'New Issue',
+ merge_request_to_resolve_discussions_of: merge_request.iid
+ }
+ end
+
+ it_behaves_like 'creating an issue resolving discussions through the API'
+ end
+
+ context 'resolving a single discussion' do
+ before do
+ post api("/projects/#{project.id}/issues", user),
+ params: {
+ title: 'New Issue',
+ merge_request_to_resolve_discussions_of: merge_request.iid,
+ discussion_to_resolve: discussion.id
+ }
+ end
+
+ it_behaves_like 'creating an issue resolving discussions through the API'
+ end
+ end
+
+ context 'with due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ post api("/projects/#{project.id}/issues", user),
+ params: { title: 'new issue', due_date: due_date }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('new issue')
+ expect(json_response['description']).to be_nil
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+
+ context 'setting created_at' do
+ let(:creation_time) { 2.weeks.ago }
+ let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } }
+
+ context 'by an admin' do
+ it 'sets the creation time on the new issue' do
+ post api("/projects/#{project.id}/issues", admin), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'by a project owner' do
+ it 'sets the creation time on the new issue' do
+ post api("/projects/#{project.id}/issues", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'by a group owner' do
+ it 'sets the creation time on the new issue' do
+ group = create(:group)
+ group_project = create(:project, :public, namespace: group)
+ group.add_owner(user2)
+ post api("/projects/#{group_project.id}/issues", user2), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
+ end
+ end
+
+ context 'by another user' do
+ it 'ignores the given creation time' do
+ post api("/projects/#{project.id}/issues", user2), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time)
+ end
+ end
+ end
+
+ context 'the user can only read the issue' do
+ it 'cannot create new labels' do
+ expect do
+ post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' }
+ end.not_to change { project.labels.count }
+ end
+
+ it 'cannot create new labels with labels param as array' do
+ expect do
+ post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: %w(label label2) }
+ end.not_to change { project.labels.count }
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/issues with spam filtering' do
+ before do
+ allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
+ allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
+ end
+
+ let(:params) do
+ {
+ title: 'new issue',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it 'does not create a new project issue' do
+ expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count)
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
+
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('new issue')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ describe '/projects/:id/issues/:issue_iid/move' do
+ let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
+ let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
+
+ it 'moves an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ params: { to_project_id: target_project.id }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['project_id']).to eq(target_project.id)
+ end
+
+ context 'when source and target projects are the same' do
+ it 'returns 400 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ params: { to_project_id: project.id }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
+ end
+ end
+
+ context 'when the user does not have the permission to move issues' do
+ it 'returns 400 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ params: { to_project_id: target_project2.id }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
+ end
+ end
+
+ it 'moves the issue to another namespace if I am admin' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
+ params: { to_project_id: target_project2.id }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['project_id']).to eq(target_project2.id)
+ end
+
+ context 'when using the issue ID instead of iid' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
+ params: { to_project_id: target_project.id }
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
+ context 'when issue does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/123/move", user),
+ params: { to_project_id: target_project.id }
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Issue Not Found')
+ end
+ end
+
+ context 'when source project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/0/issues/#{issue.iid}/move", user),
+ params: { to_project_id: target_project.id }
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Project Not Found')
+ end
+ end
+
+ context 'when target project does not exist' do
+ it 'returns 404 when trying to move an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
+ params: { to_project_id: 0 }
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ describe 'POST :id/issues/:issue_iid/subscribe' do
+ it 'subscribes to an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['subscribed']).to eq(true)
+ end
+
+ it 'returns 304 if already subscribed' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post api("/projects/#{project.id}/issues/123/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the issue ID is used instead of the iid' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ describe 'POST :id/issues/:issue_id/unsubscribe' do
+ it 'unsubscribes from an issue' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['subscribed']).to eq(false)
+ end
+
+ it 'returns 304 if not subscribed' do
+ post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
+
+ expect(response).to have_gitlab_http_status(304)
+ end
+
+ it 'returns 404 if the issue is not found' do
+ post api("/projects/#{project.id}/issues/123/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if using the issue ID instead of iid' do
+ post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 if the issue is confidential' do
+ post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/issues/put_projects_issues_spec.rb b/spec/requests/api/issues/put_projects_issues_spec.rb
new file mode 100644
index 00000000000..267cba93713
--- /dev/null
+++ b/spec/requests/api/issues/put_projects_issues_spec.rb
@@ -0,0 +1,392 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::Issues do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ set(:guest) { create(:user) }
+ set(:author) { create(:author) }
+ set(:assignee) { create(:assignee) }
+ let(:admin) { create(:user, :admin) }
+ let(:issue_title) { 'foo' }
+ let(:issue_description) { 'closed' }
+ let!(:closed_issue) do
+ create :closed_issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ state: :closed,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 3.hours.ago,
+ closed_at: 1.hour.ago
+ end
+ let!(:confidential_issue) do
+ create :issue,
+ :confidential,
+ project: project,
+ author: author,
+ assignees: [assignee],
+ created_at: generate(:past_time),
+ updated_at: 2.hours.ago
+ end
+ let!(:issue) do
+ create :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ end
+ set(:label) do
+ create(:label, title: 'label', color: '#FFAABB', project: project)
+ end
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+ let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ set(:empty_milestone) do
+ create(:milestone, title: '2.0.0', project: project)
+ end
+ let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
+
+ let(:no_milestone_title) { 'None' }
+ let(:any_milestone_title) { 'Any' }
+
+ before(:all) do
+ project.add_reporter(user)
+ project.add_guest(guest)
+ end
+
+ before do
+ stub_licensed_features(multiple_issue_assignees: false, issue_weights: false)
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update only title' do
+ it 'updates a project issue' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'returns 404 error if issue iid not found' do
+ put api("/projects/#{project.id}/issues/44444", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 error if issue id is used instead of the iid' do
+ put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'allows special label names' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: {
+ title: 'updated title',
+ labels: 'label, label?, label&foo, ?, &'
+ }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'allows special label names with labels param as array' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: {
+ title: 'updated title',
+ labels: ['label', 'label?', 'label&foo, ?, &']
+ }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ context 'confidential issues' do
+ it 'returns 403 for non project members' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'returns 403 for project members with guest role' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(403)
+ end
+
+ it 'updates a confidential issue for project members' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'updates a confidential issue for author' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'updates a confidential issue for admin' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['title']).to eq('updated title')
+ end
+
+ it 'sets an issue to confidential' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { confidential: true }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['confidential']).to be_truthy
+ end
+
+ it 'makes a confidential issue public' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
+ params: { confidential: false }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['confidential']).to be_falsy
+ end
+
+ it 'does not update a confidential issue with wrong confidential flag' do
+ put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
+ params: { confidential: 'foo' }
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['error']).to eq('confidential is invalid')
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
+ let(:params) do
+ {
+ title: 'updated title',
+ description: 'content here',
+ labels: 'label, label2'
+ }
+ end
+
+ it 'does not create a new project issue' do
+ allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
+ allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
+
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq({ 'error' => 'Spam detected' })
+
+ spam_logs = SpamLog.all
+ expect(spam_logs.count).to eq(1)
+ expect(spam_logs[0].title).to eq('updated title')
+ expect(spam_logs[0].description).to eq('content here')
+ expect(spam_logs[0].user).to eq(user)
+ expect(spam_logs[0].noteable_type).to eq('Issue')
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
+ context 'support for deprecated assignee_id' do
+ it 'removes assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_id: 0 }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignee']).to be_nil
+ end
+
+ it 'updates an issue with new assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_id: user2.id }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ end
+ end
+
+ it 'removes assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_ids: [0] }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignees']).to be_empty
+ end
+
+ it 'updates an issue with new assignee' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_ids: [user2.id] }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ context 'single assignee restrictions' do
+ it 'updates an issue with several assignees but only one has been applied' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { assignee_ids: [user2.id, guest.id] }
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['assignees'].size).to eq(1)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
+ let!(:label) { create(:label, title: 'dummy', project: project) }
+ let!(:label_link) { create(:label_link, label: label, target: issue) }
+
+ it 'does not update labels if not present' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { title: 'updated title' }
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to eq([label.title])
+ end
+
+ it 'removes all labels and touches the record' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' }
+ end
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to eq([])
+ expect(json_response['updated_at']).to be > Time.now
+ end
+
+ it 'removes all labels and touches the record with labels param as array' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: [''] }
+ end
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to eq([])
+ expect(json_response['updated_at']).to be > Time.now
+ end
+
+ it 'updates labels and touches the record' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: 'foo,bar' }
+ end
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ expect(json_response['updated_at']).to be > Time.now
+ end
+
+ it 'updates labels and touches the record with labels param as array' do
+ Timecop.travel(1.minute.from_now) do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: %w(foo bar) }
+ end
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to include 'foo'
+ expect(json_response['labels']).to include 'bar'
+ expect(json_response['updated_at']).to be > Time.now
+ end
+
+ it 'allows special label names' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label:foo'
+ expect(json_response['labels']).to include 'label-bar'
+ expect(json_response['labels']).to include 'label_bar'
+ expect(json_response['labels']).to include 'label/bar'
+ expect(json_response['labels']).to include 'label?bar'
+ expect(json_response['labels']).to include 'label&bar'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'allows special label names with labels param as array' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: ['label:foo', 'label-bar', 'label_bar', 'label/bar,label?bar,label&bar,?,&'] }
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label:foo'
+ expect(json_response['labels']).to include 'label-bar'
+ expect(json_response['labels']).to include 'label_bar'
+ expect(json_response['labels']).to include 'label/bar'
+ expect(json_response['labels']).to include 'label?bar'
+ expect(json_response['labels']).to include 'label&bar'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'returns 400 if title is too long' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { title: 'g' * 256 }
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']['title']).to eq([
+ 'is too long (maximum is 255 characters)'
+ ])
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update state and label' do
+ it 'updates a project issue' do
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: 'label2', state_event: 'close' }
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response['labels']).to include 'label2'
+ expect(json_response['state']).to eq 'closed'
+ end
+
+ it 'reopens a project isssue' do
+ put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['state']).to eq 'opened'
+ end
+
+ context 'when an admin or owner makes the request' do
+ it 'accepts the update date to be set' do
+ update_time = 2.weeks.ago
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user),
+ params: { labels: 'label3', state_event: 'close', updated_at: update_time }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['labels']).to include 'label3'
+ expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
+ it 'creates a new project issue' do
+ due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
+
+ put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['due_date']).to eq(due_date)
+ end
+ end
+end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
deleted file mode 100644
index 1a4be2bd30f..00000000000
--- a/spec/requests/api/issues_spec.rb
+++ /dev/null
@@ -1,2041 +0,0 @@
-require 'spec_helper'
-
-describe API::Issues do
- set(:user) { create(:user) }
- set(:project) do
- create(:project, :public, creator_id: user.id, namespace: user.namespace)
- end
-
- let(:user2) { create(:user) }
- let(:non_member) { create(:user) }
- set(:guest) { create(:user) }
- set(:author) { create(:author) }
- set(:assignee) { create(:assignee) }
- let(:admin) { create(:user, :admin) }
- let(:issue_title) { 'foo' }
- let(:issue_description) { 'closed' }
- let!(:closed_issue) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: project,
- state: :closed,
- milestone: milestone,
- created_at: generate(:past_time),
- updated_at: 3.hours.ago,
- closed_at: 1.hour.ago
- end
- let!(:confidential_issue) do
- create :issue,
- :confidential,
- project: project,
- author: author,
- assignees: [assignee],
- created_at: generate(:past_time),
- updated_at: 2.hours.ago
- end
- let!(:issue) do
- create :issue,
- author: user,
- assignees: [user],
- project: project,
- milestone: milestone,
- created_at: generate(:past_time),
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
- end
- set(:label) do
- create(:label, title: 'label', color: '#FFAABB', project: project)
- end
- let!(:label_link) { create(:label_link, label: label, target: issue) }
- set(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- set(:empty_milestone) do
- create(:milestone, title: '2.0.0', project: project)
- end
- let!(:note) { create(:note_on_issue, author: user, project: project, noteable: issue) }
-
- let(:no_milestone_title) { "None" }
- let(:any_milestone_title) { "Any" }
-
- before(:all) do
- project.add_reporter(user)
- project.add_guest(guest)
- end
-
- describe "GET /issues" do
- context "when unauthenticated" do
- it "returns an array of all issues" do
- get api("/issues"), params: { scope: 'all' }
-
- expect(response).to have_http_status(200)
- expect(json_response).to be_an Array
- end
-
- it "returns authentication error without any scope" do
- get api("/issues")
-
- expect(response).to have_http_status(401)
- end
-
- it "returns authentication error when scope is assigned-to-me" do
- get api("/issues"), params: { scope: 'assigned-to-me' }
-
- expect(response).to have_http_status(401)
- end
-
- it "returns authentication error when scope is created-by-me" do
- get api("/issues"), params: { scope: 'created-by-me' }
-
- expect(response).to have_http_status(401)
- end
- end
-
- context "when authenticated" do
- it "returns an array of issues" do
- get api("/issues", user)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- expect(json_response.first['title']).to eq(issue.title)
- expect(json_response.last).to have_key('web_url')
- end
-
- it 'returns an array of closed issues' do
- get api('/issues', user), params: { state: :closed }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of opened issues' do
- get api('/issues', user), params: { state: :opened }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of all issues' do
- get api('/issues', user), params: { state: :all }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns issues assigned to me' do
- issue2 = create(:issue, assignees: [user2], project: project)
-
- get api('/issues', user2), params: { scope: 'assigned_to_me' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues assigned to me (kebab-case)' do
- issue2 = create(:issue, assignees: [user2], project: project)
-
- get api('/issues', user2), params: { scope: 'assigned-to-me' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues authored by the given author id' do
- issue2 = create(:issue, author: user2, project: project)
-
- get api('/issues', user), params: { author_id: user2.id, scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues assigned to the given assignee id' do
- issue2 = create(:issue, assignees: [user2], project: project)
-
- get api('/issues', user), params: { assignee_id: user2.id, scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues authored by the given author id and assigned to the given assignee id' do
- issue2 = create(:issue, author: user2, assignees: [user2], project: project)
-
- get api('/issues', user), params: { author_id: user2.id, assignee_id: user2.id, scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues with no assignee' do
- issue2 = create(:issue, author: user2, project: project)
-
- get api('/issues', user), params: { assignee_id: 0, scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues with no assignee' do
- issue2 = create(:issue, author: user2, project: project)
-
- get api('/issues', user), params: { assignee_id: 'None', scope: 'all' }
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues with any assignee' do
- # This issue without assignee should not be returned
- create(:issue, author: user2, project: project)
-
- get api('/issues', user), params: { assignee_id: 'Any', scope: 'all' }
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns only confidential issues' do
- get api('/issues', user), params: { confidential: true, scope: 'all' }
-
- expect_paginated_array_response(confidential_issue.id)
- end
-
- it 'returns only public issues' do
- get api('/issues', user), params: { confidential: false }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns issues reacted by the authenticated user' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
- create(:award_emoji, awardable: issue2, user: user2, name: 'star')
- create(:award_emoji, awardable: issue, user: user2, name: 'thumbsup')
-
- get api('/issues', user2), params: { my_reaction_emoji: 'Any', scope: 'all' }
-
- expect_paginated_array_response([issue2.id, issue.id])
- end
-
- it 'returns issues not reacted by the authenticated user' do
- issue2 = create(:issue, project: project, author: user, assignees: [user])
- create(:award_emoji, awardable: issue2, user: user2, name: 'star')
-
- get api('/issues', user2), params: { my_reaction_emoji: 'None', scope: 'all' }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns issues matching given search string for title' do
- get api("/issues", user), params: { search: issue.title }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns issues matching given search string for title and scoped in title' do
- get api("/issues", user), params: { search: issue.title, in: 'title' }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an empty array if no issue matches given search string for title and scoped in description' do
- get api("/issues", user), params: { search: issue.title, in: 'description' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns issues matching given search string for description' do
- get api("/issues", user), params: { search: issue.description }
-
- expect_paginated_array_response(issue.id)
- end
-
- context 'filtering before a specific date' do
- let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) }
-
- it 'returns issues created before a specific date' do
- get api('/issues?created_before=2000-01-02T00:00:00.060Z', user)
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues updated before a specific date' do
- get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user)
-
- expect_paginated_array_response(issue2.id)
- end
- end
-
- context 'filtering after a specific date' do
- let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) }
-
- it 'returns issues created after a specific date' do
- get api("/issues?created_after=#{issue2.created_at}", user)
-
- expect_paginated_array_response(issue2.id)
- end
-
- it 'returns issues updated after a specific date' do
- get api("/issues?updated_after=#{issue2.updated_at}", user)
-
- expect_paginated_array_response(issue2.id)
- end
- end
-
- it 'returns an array of labeled issues' do
- get api("/issues", user), params: { labels: label.title }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label.title])
- end
-
- it 'returns an array of labeled issues when all labels matches' do
- label_b = create(:label, title: 'foo', project: project)
- label_c = create(:label, title: 'bar', project: project)
-
- create(:label_link, label: label_b, target: issue)
- create(:label_link, label: label_c, target: issue)
-
- get api("/issues", user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
- end
-
- it 'returns an empty array if no issue matches labels' do
- get api('/issues', user), params: { labels: 'foo,bar' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of labeled issues matching given state' do
- get api("/issues", user), params: { labels: label.title, state: :opened }
-
- expect_paginated_array_response(issue.id)
- expect(json_response.first['labels']).to eq([label.title])
- expect(json_response.first['state']).to eq('opened')
- end
-
- it 'returns an empty array if no issue matches labels and state filters' do
- get api("/issues", user), params: { labels: label.title, state: :closed }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of issues with any label' do
- get api("/issues", user), params: { labels: IssuesFinder::FILTER_ANY }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of issues with no label' do
- get api("/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of issues with no label when using the legacy No+Label filter' do
- get api("/issues", user), params: { labels: "No Label" }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api("/issues?milestone=#{empty_milestone.title}", user)
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api("/issues?milestone=foo", user)
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of issues in given milestone' do
- get api("/issues?milestone=#{milestone.title}", user)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns an array of issues matching state in milestone' do
- get api("/issues?milestone=#{milestone.title}"\
- '&state=closed', user)
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of issues with no milestone' do
- get api("/issues?milestone=#{no_milestone_title}", author)
-
- expect_paginated_array_response(confidential_issue.id)
- end
-
- it 'returns an array of issues found by iids' do
- get api('/issues', user), params: { iids: [closed_issue.iid] }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an empty array if iid does not exist' do
- get api("/issues", user), params: { iids: [99999] }
-
- expect_paginated_array_response([])
- end
-
- context 'without sort params' do
- it 'sorts by created_at descending by default' do
- get api('/issues', user)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- context 'with 2 issues with same created_at' do
- let!(:closed_issue2) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: project,
- milestone: milestone,
- created_at: closed_issue.created_at,
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
- end
-
- it 'page breaks first page correctly' do
- get api('/issues?per_page=2', user)
-
- expect_paginated_array_response([issue.id, closed_issue2.id])
- end
-
- it 'page breaks second page correctly' do
- get api('/issues?per_page=2&page=2', user)
-
- expect_paginated_array_response([closed_issue.id])
- end
- end
- end
-
- it 'sorts ascending when requested' do
- get api('/issues?sort=asc', user)
-
- expect_paginated_array_response([closed_issue.id, issue.id])
- end
-
- it 'sorts by updated_at descending when requested' do
- get api('/issues?order_by=updated_at', user)
-
- issue.touch(:updated_at)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'sorts by updated_at ascending when requested' do
- get api('/issues?order_by=updated_at&sort=asc', user)
-
- issue.touch(:updated_at)
-
- expect_paginated_array_response([closed_issue.id, issue.id])
- end
-
- it 'matches V4 response schema' do
- get api('/issues', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/issues')
- end
-
- it 'returns a related merge request count of 0 if there are no related merge requests' do
- get api('/issues', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/issues')
- expect(json_response.first).to include('merge_requests_count' => 0)
- end
-
- it 'returns a related merge request count > 0 if there are related merge requests' do
- create(:merge_requests_closing_issues, issue: issue)
-
- get api('/issues', user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/issues')
- expect(json_response.first).to include('merge_requests_count' => 1)
- end
- end
- end
-
- describe "GET /groups/:id/issues" do
- let!(:group) { create(:group) }
- let!(:group_project) { create(:project, :public, creator_id: user.id, namespace: group) }
- let!(:group_closed_issue) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: group_project,
- state: :closed,
- milestone: group_milestone,
- updated_at: 3.hours.ago,
- created_at: 1.day.ago
- end
- let!(:group_confidential_issue) do
- create :issue,
- :confidential,
- project: group_project,
- author: author,
- assignees: [assignee],
- updated_at: 2.hours.ago,
- created_at: 2.days.ago
- end
- let!(:group_issue) do
- create :issue,
- author: user,
- assignees: [user],
- project: group_project,
- milestone: group_milestone,
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description,
- created_at: 5.days.ago
- end
- let!(:group_label) do
- create(:label, title: 'group_lbl', color: '#FFAABB', project: group_project)
- end
- let!(:group_label_link) { create(:label_link, label: group_label, target: group_issue) }
- let!(:group_milestone) { create(:milestone, title: '3.0.0', project: group_project) }
- let!(:group_empty_milestone) do
- create(:milestone, title: '4.0.0', project: group_project)
- end
- let!(:group_note) { create(:note_on_issue, author: user, project: group_project, noteable: group_issue) }
-
- let(:base_url) { "/groups/#{group.id}/issues" }
-
- context 'when group has subgroups', :nested_groups do
- let(:subgroup_1) { create(:group, parent: group) }
- let(:subgroup_2) { create(:group, parent: subgroup_1) }
-
- let(:subgroup_1_project) { create(:project, namespace: subgroup_1) }
- let(:subgroup_2_project) { create(:project, namespace: subgroup_2) }
-
- let!(:issue_1) { create(:issue, project: subgroup_1_project) }
- let!(:issue_2) { create(:issue, project: subgroup_2_project) }
-
- before do
- group.add_developer(user)
- end
-
- it 'also returns subgroups projects issues' do
- get api(base_url, user)
-
- expect_paginated_array_response([issue_2.id, issue_1.id, group_closed_issue.id, group_confidential_issue.id, group_issue.id])
- end
- end
-
- context 'when user is unauthenticated' do
- it 'lists all issues in public projects' do
- get api(base_url)
-
- expect_paginated_array_response([group_closed_issue.id, group_issue.id])
- end
- end
-
- context 'when user is a group member' do
- before do
- group_project.add_reporter(user)
- end
-
- it 'returns all group issues (including opened and closed)' do
- get api(base_url, admin)
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns group issues without confidential issues for non project members' do
- get api(base_url, non_member), params: { state: :opened }
-
- expect_paginated_array_response(group_issue.id)
- end
-
- it 'returns group confidential issues for author' do
- get api(base_url, author), params: { state: :opened }
-
- expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns group confidential issues for assignee' do
- get api(base_url, assignee), params: { state: :opened }
-
- expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns group issues with confidential issues for project members' do
- get api(base_url, user), params: { state: :opened }
-
- expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns group confidential issues for admin' do
- get api(base_url, admin), params: { state: :opened }
-
- expect_paginated_array_response([group_confidential_issue.id, group_issue.id])
- end
-
- it 'returns only confidential issues' do
- get api(base_url, user), params: { confidential: true }
-
- expect_paginated_array_response(group_confidential_issue.id)
- end
-
- it 'returns only public issues' do
- get api(base_url, user), params: { confidential: false }
-
- expect_paginated_array_response([group_closed_issue.id, group_issue.id])
- end
-
- it 'returns an array of labeled group issues' do
- get api(base_url, user), params: { labels: group_label.title }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['labels']).to eq([group_label.title])
- end
-
- it 'returns an array of labeled group issues where all labels match' do
- get api(base_url, user), params: { labels: "#{group_label.title},foo,bar" }
-
- expect_paginated_array_response([])
- end
-
- it 'returns issues matching given search string for title' do
- get api(base_url, user), params: { search: group_issue.title }
-
- expect_paginated_array_response(group_issue.id)
- end
-
- it 'returns issues matching given search string for description' do
- get api(base_url, user), params: { search: group_issue.description }
-
- expect_paginated_array_response(group_issue.id)
- end
-
- it 'returns an array of labeled issues when all labels matches' do
- label_b = create(:label, title: 'foo', project: group_project)
- label_c = create(:label, title: 'bar', project: group_project)
-
- create(:label_link, label: label_b, target: group_issue)
- create(:label_link, label: label_c, target: group_issue)
-
- get api(base_url, user), params: { labels: "#{group_label.title},#{label_b.title},#{label_c.title}" }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['labels']).to eq([label_c.title, label_b.title, group_label.title])
- end
-
- it 'returns an array of issues found by iids' do
- get api(base_url, user), params: { iids: [group_issue.iid] }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
-
- it 'returns an empty array if iid does not exist' do
- get api(base_url, user), params: { iids: [99999] }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if no group issue matches labels' do
- get api(base_url, user), params: { labels: 'foo,bar' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of group issues with any label' do
- get api(base_url, user), params: { labels: IssuesFinder::FILTER_ANY }
-
- expect_paginated_array_response(group_issue.id)
- expect(json_response.first['id']).to eq(group_issue.id)
- end
-
- it 'returns an array of group issues with no label' do
- get api(base_url, user), params: { labels: IssuesFinder::FILTER_NONE }
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id])
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api(base_url, user), params: { milestone: group_empty_milestone.title }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api(base_url, user), params: { milestone: 'foo' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of issues in given milestone' do
- get api(base_url, user), params: { state: :opened, milestone: group_milestone.title }
-
- expect_paginated_array_response(group_issue.id)
- end
-
- it 'returns an array of issues matching state in milestone' do
- get api(base_url, user), params: { milestone: group_milestone.title, state: :closed }
-
- expect_paginated_array_response(group_closed_issue.id)
- end
-
- it 'returns an array of issues with no milestone' do
- get api(base_url, user), params: { milestone: no_milestone_title }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect_paginated_array_response(group_confidential_issue.id)
- end
-
- context 'without sort params' do
- it 'sorts by created_at descending by default' do
- get api(base_url, user)
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
- end
-
- context 'with 2 issues with same created_at' do
- let!(:group_issue2) do
- create :issue,
- author: user,
- assignees: [user],
- project: group_project,
- milestone: group_milestone,
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description,
- created_at: group_issue.created_at
- end
-
- it 'page breaks first page correctly' do
- get api("#{base_url}?per_page=3", user)
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue2.id])
- end
-
- it 'page breaks second page correctly' do
- get api("#{base_url}?per_page=3&page=2", user)
-
- expect_paginated_array_response([group_issue.id])
- end
- end
- end
-
- it 'sorts ascending when requested' do
- get api("#{base_url}?sort=asc", user)
-
- expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
- end
-
- it 'sorts by updated_at descending when requested' do
- get api("#{base_url}?order_by=updated_at", user)
-
- group_issue.touch(:updated_at)
-
- expect_paginated_array_response([group_issue.id, group_confidential_issue.id, group_closed_issue.id])
- end
-
- it 'sorts by updated_at ascending when requested' do
- get api(base_url, user), params: { order_by: :updated_at, sort: :asc }
-
- expect_paginated_array_response([group_closed_issue.id, group_confidential_issue.id, group_issue.id])
- end
- end
- end
-
- describe "GET /projects/:id/issues" do
- let(:base_url) { "/projects/#{project.id}" }
-
- context 'when unauthenticated' do
- it 'returns public project issues' do
- get api("/projects/#{project.id}/issues")
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
- end
-
- it 'avoids N+1 queries' do
- get api("/projects/#{project.id}/issues", user)
-
- control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
- get api("/projects/#{project.id}/issues", user)
- end.count
-
- create_list(:issue, 3, project: project)
-
- expect do
- get api("/projects/#{project.id}/issues", user)
- end.not_to exceed_all_query_limit(control_count)
- end
-
- it 'returns 404 when project does not exist' do
- get api('/projects/1000/issues', non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 on private projects for other users" do
- private_project = create(:project, :private)
- create(:issue, project: private_project)
-
- get api("/projects/#{private_project.id}/issues", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns no issues when user has access to project but not issues' do
- restricted_project = create(:project, :public, :issues_private)
- create(:issue, project: restricted_project)
-
- get api("/projects/#{restricted_project.id}/issues", non_member)
-
- expect_paginated_array_response([])
- end
-
- it 'returns project issues without confidential issues for non project members' do
- get api("#{base_url}/issues", non_member)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns project issues without confidential issues for project members with guest role' do
- get api("#{base_url}/issues", guest)
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns project confidential issues for author' do
- get api("#{base_url}/issues", author)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns only confidential issues' do
- get api("#{base_url}/issues", author), params: { confidential: true }
-
- expect_paginated_array_response(confidential_issue.id)
- end
-
- it 'returns only public issues' do
- get api("#{base_url}/issues", author), params: { confidential: false }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns project confidential issues for assignee' do
- get api("#{base_url}/issues", assignee)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns project issues with confidential issues for project members' do
- get api("#{base_url}/issues", user)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns project confidential issues for admin' do
- get api("#{base_url}/issues", admin)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'returns an array of labeled project issues' do
- get api("#{base_url}/issues", user), params: { labels: label.title }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of labeled issues when all labels matches' do
- label_b = create(:label, title: 'foo', project: project)
- label_c = create(:label, title: 'bar', project: project)
-
- create(:label_link, label: label_b, target: issue)
- create(:label_link, label: label_c, target: issue)
-
- get api("#{base_url}/issues", user), params: { labels: "#{label.title},#{label_b.title},#{label_c.title}" }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns issues matching given search string for title' do
- get api("#{base_url}/issues?search=#{issue.title}", user)
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns issues matching given search string for description' do
- get api("#{base_url}/issues?search=#{issue.description}", user)
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of issues found by iids' do
- get api("#{base_url}/issues", user), params: { iids: [issue.iid] }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an empty array if iid does not exist' do
- get api("#{base_url}/issues", user), params: { iids: [99999] }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if not all labels matches' do
- get api("#{base_url}/issues?labels=#{label.title},foo", user)
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of project issues with any label' do
- get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_ANY }
-
- expect_paginated_array_response(issue.id)
- end
-
- it 'returns an array of project issues with no label' do
- get api("#{base_url}/issues", user), params: { labels: IssuesFinder::FILTER_NONE }
-
- expect_paginated_array_response([confidential_issue.id, closed_issue.id])
- end
-
- it 'returns an empty array if no project issue matches labels' do
- get api("#{base_url}/issues", user), params: { labels: 'foo,bar' }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api("#{base_url}/issues", user), params: { milestone: empty_milestone.title }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api("#{base_url}/issues", user), params: { milestone: :foo }
-
- expect_paginated_array_response([])
- end
-
- it 'returns an array of issues in given milestone' do
- get api("#{base_url}/issues", user), params: { milestone: milestone.title }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- it 'returns an array of issues matching state in milestone' do
- get api("#{base_url}/issues", user), params: { milestone: milestone.title, state: :closed }
-
- expect_paginated_array_response(closed_issue.id)
- end
-
- it 'returns an array of issues with no milestone' do
- get api("#{base_url}/issues", user), params: { milestone: no_milestone_title }
-
- expect_paginated_array_response(confidential_issue.id)
- end
-
- it 'returns an array of issues with any milestone' do
- get api("#{base_url}/issues", user), params: { milestone: any_milestone_title }
-
- expect_paginated_array_response([issue.id, closed_issue.id])
- end
-
- context 'without sort params' do
- it 'sorts by created_at descending by default' do
- get api("#{base_url}/issues", user)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- context 'with 2 issues with same created_at' do
- let!(:closed_issue2) do
- create :closed_issue,
- author: user,
- assignees: [user],
- project: project,
- milestone: milestone,
- created_at: closed_issue.created_at,
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
- end
-
- it 'page breaks first page correctly' do
- get api("#{base_url}/issues?per_page=3", user)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue2.id])
- end
-
- it 'page breaks second page correctly' do
- get api("#{base_url}/issues?per_page=3&page=2", user)
-
- expect_paginated_array_response([closed_issue.id])
- end
- end
- end
-
- it 'sorts ascending when requested' do
- get api("#{base_url}/issues", user), params: { sort: :asc }
-
- expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
- end
-
- it 'sorts by updated_at descending when requested' do
- get api("#{base_url}/issues", user), params: { order_by: :updated_at }
-
- issue.touch(:updated_at)
-
- expect_paginated_array_response([issue.id, confidential_issue.id, closed_issue.id])
- end
-
- it 'sorts by updated_at ascending when requested' do
- get api("#{base_url}/issues", user), params: { order_by: :updated_at, sort: :asc }
-
- expect_paginated_array_response([closed_issue.id, confidential_issue.id, issue.id])
- end
- end
-
- describe "GET /projects/:id/issues/:issue_iid" do
- context 'when unauthenticated' do
- it 'returns public issues' do
- get api("/projects/#{project.id}/issues/#{issue.iid}")
-
- expect(response).to have_gitlab_http_status(200)
- end
- end
-
- it 'exposes known attributes' do
- get api("/projects/#{project.id}/issues/#{issue.iid}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['id']).to eq(issue.id)
- expect(json_response['iid']).to eq(issue.iid)
- expect(json_response['project_id']).to eq(issue.project.id)
- expect(json_response['title']).to eq(issue.title)
- expect(json_response['description']).to eq(issue.description)
- expect(json_response['state']).to eq(issue.state)
- expect(json_response['closed_at']).to be_falsy
- expect(json_response['created_at']).to be_present
- expect(json_response['updated_at']).to be_present
- expect(json_response['labels']).to eq(issue.label_names)
- expect(json_response['milestone']).to be_a Hash
- expect(json_response['assignees']).to be_a Array
- expect(json_response['assignee']).to be_a Hash
- expect(json_response['author']).to be_a Hash
- expect(json_response['confidential']).to be_falsy
- end
-
- it "exposes the 'closed_at' attribute" do
- get api("/projects/#{project.id}/issues/#{closed_issue.iid}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['closed_at']).to be_present
- end
-
- context 'links exposure' do
- it 'exposes related resources full URIs' do
- get api("/projects/#{project.id}/issues/#{issue.iid}", user)
-
- links = json_response['_links']
-
- expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
- expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
- expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
- expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
- end
- end
-
- it "returns a project issue by internal id" do
- get api("/projects/#{project.id}/issues/#{issue.iid}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(issue.title)
- expect(json_response['iid']).to eq(issue.iid)
- end
-
- it "returns 404 if issue id not found" do
- get api("/projects/#{project.id}/issues/54321", user)
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 if the issue ID is used" do
- get api("/projects/#{project.id}/issues/#{issue.id}", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- context 'confidential issues' do
- it "returns 404 for non project members" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 for project members with guest role" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns confidential issue for project members" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
-
- it "returns confidential issue for author" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
-
- it "returns confidential issue for assignee" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", assignee)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
-
- it "returns confidential issue for admin" do
- get api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(confidential_issue.title)
- expect(json_response['iid']).to eq(confidential_issue.iid)
- end
- end
- end
-
- describe "POST /projects/:id/issues" do
- context 'support for deprecated assignee_id' do
- it 'creates a new project issue' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', assignee_id: user2.id }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['assignee']['name']).to eq(user2.name)
- expect(json_response['assignees'].first['name']).to eq(user2.name)
- end
-
- it 'creates a new project issue when assignee_id is empty' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', assignee_id: '' }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['assignee']).to be_nil
- end
- end
-
- context 'single assignee restrictions' do
- it 'creates a new project issue with no more than one assignee' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', assignee_ids: [user2.id, guest.id] }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['assignees'].count).to eq(1)
- end
- end
-
- context 'user does not have permissions to create issue' do
- let(:not_member) { create(:user) }
-
- before do
- project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE)
- end
-
- it 'renders 403' do
- post api("/projects/#{project.id}/issues", not_member), params: { title: 'new issue' }
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- context 'an internal ID is provided' do
- context 'by an admin' do
- it 'sets the internal ID on the new issue' do
- post api("/projects/#{project.id}/issues", admin),
- params: { title: 'new issue', iid: 9001 }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['iid']).to eq 9001
- end
- end
-
- context 'by an owner' do
- it 'sets the internal ID on the new issue' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', iid: 9001 }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['iid']).to eq 9001
- end
- end
-
- context 'by a group owner' do
- let(:group) { create(:group) }
- let(:group_project) { create(:project, :public, namespace: group) }
-
- it 'sets the internal ID on the new issue' do
- group.add_owner(user2)
- post api("/projects/#{group_project.id}/issues", user2),
- params: { title: 'new issue', iid: 9001 }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['iid']).to eq 9001
- end
- end
-
- context 'by another user' do
- it 'ignores the given internal ID' do
- post api("/projects/#{project.id}/issues", user2),
- params: { title: 'new issue', iid: 9001 }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['iid']).not_to eq 9001
- end
- end
- end
-
- it 'creates a new project issue' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', labels: 'label, label2', weight: 3, assignee_ids: [user2.id] }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['description']).to be_nil
- expect(json_response['labels']).to eq(%w(label label2))
- expect(json_response['confidential']).to be_falsy
- expect(json_response['assignee']['name']).to eq(user2.name)
- expect(json_response['assignees'].first['name']).to eq(user2.name)
- end
-
- it 'creates a new confidential project issue' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', confidential: true }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'creates a new confidential project issue with a different param' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', confidential: 'y' }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'creates a public issue when confidential param is false' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', confidential: false }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['confidential']).to be_falsy
- end
-
- it 'creates a public issue when confidential param is invalid' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', confidential: 'foo' }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('confidential is invalid')
- end
-
- it "returns a 400 bad request if title not given" do
- post api("/projects/#{project.id}/issues", user), params: { labels: 'label, label2' }
- expect(response).to have_gitlab_http_status(400)
- end
-
- it 'allows special label names' do
- post api("/projects/#{project.id}/issues", user),
- params: {
- title: 'new issue',
- labels: 'label, label?, label&foo, ?, &'
- }
- expect(response.status).to eq(201)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'returns 400 if title is too long' do
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'g' * 256 }
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['title']).to eq([
- 'is too long (maximum is 255 characters)'
- ])
- end
-
- context 'resolving discussions' do
- let(:discussion) { create(:diff_note_on_merge_request).to_discussion }
- let(:merge_request) { discussion.noteable }
- let(:project) { merge_request.source_project }
-
- before do
- project.add_maintainer(user)
- end
-
- context 'resolving all discussions in a merge request' do
- before do
- post api("/projects/#{project.id}/issues", user),
- params: {
- title: 'New Issue',
- merge_request_to_resolve_discussions_of: merge_request.iid
- }
- end
-
- it_behaves_like 'creating an issue resolving discussions through the API'
- end
-
- context 'resolving a single discussion' do
- before do
- post api("/projects/#{project.id}/issues", user),
- params: {
- title: 'New Issue',
- merge_request_to_resolve_discussions_of: merge_request.iid,
- discussion_to_resolve: discussion.id
- }
- end
-
- it_behaves_like 'creating an issue resolving discussions through the API'
- end
- end
-
- context 'with due date' do
- it 'creates a new project issue' do
- due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
-
- post api("/projects/#{project.id}/issues", user),
- params: { title: 'new issue', due_date: due_date }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('new issue')
- expect(json_response['description']).to be_nil
- expect(json_response['due_date']).to eq(due_date)
- end
- end
-
- context 'setting created_at' do
- let(:creation_time) { 2.weeks.ago }
- let(:params) { { title: 'new issue', labels: 'label, label2', created_at: creation_time } }
-
- context 'by an admin' do
- it 'sets the creation time on the new issue' do
- post api("/projects/#{project.id}/issues", admin), params: params
-
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
- end
- end
-
- context 'by a project owner' do
- it 'sets the creation time on the new issue' do
- post api("/projects/#{project.id}/issues", user), params: params
-
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
- end
- end
-
- context 'by a group owner' do
- it 'sets the creation time on the new issue' do
- group = create(:group)
- group_project = create(:project, :public, namespace: group)
- group.add_owner(user2)
- post api("/projects/#{group_project.id}/issues", user2), params: params
-
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).to be_like_time(creation_time)
- end
- end
-
- context 'by another user' do
- it 'ignores the given creation time' do
- post api("/projects/#{project.id}/issues", user2), params: params
-
- expect(response).to have_gitlab_http_status(201)
- expect(Time.parse(json_response['created_at'])).not_to be_like_time(creation_time)
- end
- end
- end
-
- context 'the user can only read the issue' do
- it 'cannot create new labels' do
- expect do
- post api("/projects/#{project.id}/issues", non_member), params: { title: 'new issue', labels: 'label, label2' }
- end.not_to change { project.labels.count }
- end
- end
- end
-
- describe 'POST /projects/:id/issues with spam filtering' do
- before do
- allow_any_instance_of(SpamService).to receive(:check_for_spam?).and_return(true)
- allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
- end
-
- let(:params) do
- {
- title: 'new issue',
- description: 'content here',
- labels: 'label, label2'
- }
- end
-
- it "does not create a new project issue" do
- expect { post api("/projects/#{project.id}/issues", user), params: params }.not_to change(Issue, :count)
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ "error" => "Spam detected" })
-
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('new issue')
- expect(spam_logs[0].description).to eq('content here')
- expect(spam_logs[0].user).to eq(user)
- expect(spam_logs[0].noteable_type).to eq('Issue')
- end
- end
-
- describe "PUT /projects/:id/issues/:issue_iid to update only title" do
- it "updates a project issue" do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['title']).to eq('updated title')
- end
-
- it "returns 404 error if issue iid not found" do
- put api("/projects/#{project.id}/issues/44444", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(404)
- end
-
- it "returns 404 error if issue id is used instead of the iid" do
- put api("/projects/#{project.id}/issues/#{issue.id}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'allows special label names' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: {
- title: 'updated title',
- labels: 'label, label?, label&foo, ?, &'
- }
-
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- context 'confidential issues' do
- it "returns 403 for non project members" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", non_member),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "returns 403 for project members with guest role" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", guest),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "updates a confidential issue for project members" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it "updates a confidential issue for author" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", author),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it "updates a confidential issue for admin" do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", admin),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('updated title')
- end
-
- it 'sets an issue to confidential' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { confidential: true }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['confidential']).to be_truthy
- end
-
- it 'makes a confidential issue public' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { confidential: false }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['confidential']).to be_falsy
- end
-
- it 'does not update a confidential issue with wrong confidential flag' do
- put api("/projects/#{project.id}/issues/#{confidential_issue.iid}", user),
- params: { confidential: 'foo' }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['error']).to eq('confidential is invalid')
- end
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_iid with spam filtering' do
- let(:params) do
- {
- title: 'updated title',
- description: 'content here',
- labels: 'label, label2'
- }
- end
-
- it "does not create a new project issue" do
- allow_any_instance_of(SpamService).to receive_messages(check_for_spam?: true)
- allow_any_instance_of(AkismetService).to receive_messages(spam?: true)
-
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: params
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq({ "error" => "Spam detected" })
-
- spam_logs = SpamLog.all
- expect(spam_logs.count).to eq(1)
- expect(spam_logs[0].title).to eq('updated title')
- expect(spam_logs[0].description).to eq('content here')
- expect(spam_logs[0].user).to eq(user)
- expect(spam_logs[0].noteable_type).to eq('Issue')
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_iid to update assignee' do
- context 'support for deprecated assignee_id' do
- it 'removes assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_id: 0 }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignee']).to be_nil
- end
-
- it 'updates an issue with new assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_id: user2.id }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignee']['name']).to eq(user2.name)
- end
- end
-
- it 'removes assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [0] }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignees']).to be_empty
- end
-
- it 'updates an issue with new assignee' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [user2.id] }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignees'].first['name']).to eq(user2.name)
- end
-
- context 'single assignee restrictions' do
- it 'updates an issue with several assignees but only one has been applied' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { assignee_ids: [user2.id, guest.id] }
-
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['assignees'].size).to eq(1)
- end
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_iid to update labels' do
- let!(:label) { create(:label, title: 'dummy', project: project) }
- let!(:label_link) { create(:label_link, label: label, target: issue) }
-
- it 'does not update labels if not present' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'updated title' }
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to eq([label.title])
- end
-
- it 'removes all labels and touches the record' do
- Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { labels: '' }
- end
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to eq([])
- expect(json_response['updated_at']).to be > Time.now
- end
-
- it 'updates labels and touches the record' do
- Timecop.travel(1.minute.from_now) do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'foo,bar' }
- end
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to include 'foo'
- expect(json_response['labels']).to include 'bar'
- expect(json_response['updated_at']).to be > Time.now
- end
-
- it 'allows special label names' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'label:foo, label-bar,label_bar,label/bar,label?bar,label&bar,?,&' }
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label:foo'
- expect(json_response['labels']).to include 'label-bar'
- expect(json_response['labels']).to include 'label_bar'
- expect(json_response['labels']).to include 'label/bar'
- expect(json_response['labels']).to include 'label?bar'
- expect(json_response['labels']).to include 'label&bar'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
- it 'returns 400 if title is too long' do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { title: 'g' * 256 }
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['title']).to eq([
- 'is too long (maximum is 255 characters)'
- ])
- end
- end
-
- describe "PUT /projects/:id/issues/:issue_iid to update state and label" do
- it "updates a project issue" do
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'label2', state_event: "close" }
- expect(response).to have_gitlab_http_status(200)
-
- expect(json_response['labels']).to include 'label2'
- expect(json_response['state']).to eq "closed"
- end
-
- it 'reopens a project isssue' do
- put api("/projects/#{project.id}/issues/#{closed_issue.iid}", user), params: { state_event: 'reopen' }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['state']).to eq 'opened'
- end
-
- context 'when an admin or owner makes the request' do
- it 'accepts the update date to be set' do
- update_time = 2.weeks.ago
- put api("/projects/#{project.id}/issues/#{issue.iid}", user),
- params: { labels: 'label3', state_event: 'close', updated_at: update_time }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['labels']).to include 'label3'
- expect(Time.parse(json_response['updated_at'])).to be_like_time(update_time)
- end
- end
- end
-
- describe 'PUT /projects/:id/issues/:issue_iid to update due date' do
- it 'creates a new project issue' do
- due_date = 2.weeks.from_now.strftime('%Y-%m-%d')
-
- put api("/projects/#{project.id}/issues/#{issue.iid}", user), params: { due_date: due_date }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['due_date']).to eq(due_date)
- end
- end
-
- describe "DELETE /projects/:id/issues/:issue_iid" do
- it "rejects a non member from deleting an issue" do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", non_member)
- expect(response).to have_gitlab_http_status(403)
- end
-
- it "rejects a developer from deleting an issue" do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", author)
- expect(response).to have_gitlab_http_status(403)
- end
-
- context "when the user is project owner" do
- let(:owner) { create(:user) }
- let(:project) { create(:project, namespace: owner.namespace) }
-
- it "deletes the issue if an admin requests it" do
- delete api("/projects/#{project.id}/issues/#{issue.iid}", owner)
-
- expect(response).to have_gitlab_http_status(204)
- end
-
- it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/issues/#{issue.iid}", owner) }
- end
- end
-
- context 'when issue does not exist' do
- it 'returns 404 when trying to move an issue' do
- delete api("/projects/#{project.id}/issues/123", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- it 'returns 404 when using the issue ID instead of IID' do
- delete api("/projects/#{project.id}/issues/#{issue.id}", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe '/projects/:id/issues/:issue_iid/move' do
- let!(:target_project) { create(:project, creator_id: user.id, namespace: user.namespace ) }
- let!(:target_project2) { create(:project, creator_id: non_member.id, namespace: non_member.namespace ) }
-
- it 'moves an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- params: { to_project_id: target_project.id }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['project_id']).to eq(target_project.id)
- end
-
- context 'when source and target projects are the same' do
- it 'returns 400 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- params: { to_project_id: project.id }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Cannot move issue to project it originates from!')
- end
- end
-
- context 'when the user does not have the permission to move issues' do
- it 'returns 400 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- params: { to_project_id: target_project2.id }
-
- expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Cannot move issue due to insufficient permissions!')
- end
- end
-
- it 'moves the issue to another namespace if I am admin' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", admin),
- params: { to_project_id: target_project2.id }
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['project_id']).to eq(target_project2.id)
- end
-
- context 'when using the issue ID instead of iid' do
- it 'returns 404 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.id}/move", user),
- params: { to_project_id: target_project.id }
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Issue Not Found')
- end
- end
-
- context 'when issue does not exist' do
- it 'returns 404 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/123/move", user),
- params: { to_project_id: target_project.id }
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Issue Not Found')
- end
- end
-
- context 'when source project does not exist' do
- it 'returns 404 when trying to move an issue' do
- post api("/projects/0/issues/#{issue.iid}/move", user),
- params: { to_project_id: target_project.id }
-
- expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Project Not Found')
- end
- end
-
- context 'when target project does not exist' do
- it 'returns 404 when trying to move an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- params: { to_project_id: 0 }
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
- end
-
- describe 'POST :id/issues/:issue_iid/subscribe' do
- it 'subscribes to an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user2)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['subscribed']).to eq(true)
- end
-
- it 'returns 304 if already subscribed' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/subscribe", user)
-
- expect(response).to have_gitlab_http_status(304)
- end
-
- it 'returns 404 if the issue is not found' do
- post api("/projects/#{project.id}/issues/123/subscribe", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 404 if the issue ID is used instead of the iid' do
- post api("/projects/#{project.id}/issues/#{issue.id}/subscribe", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/subscribe", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'POST :id/issues/:issue_id/unsubscribe' do
- it 'unsubscribes from an issue' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user)
-
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['subscribed']).to eq(false)
- end
-
- it 'returns 304 if not subscribed' do
- post api("/projects/#{project.id}/issues/#{issue.iid}/unsubscribe", user2)
-
- expect(response).to have_gitlab_http_status(304)
- end
-
- it 'returns 404 if the issue is not found' do
- post api("/projects/#{project.id}/issues/123/unsubscribe", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 404 if using the issue ID instead of iid' do
- post api("/projects/#{project.id}/issues/#{issue.id}/unsubscribe", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
-
- it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/unsubscribe", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'time tracking endpoints' do
- let(:issuable) { issue }
-
- include_examples 'time tracking endpoints', 'issue'
- end
-
- describe 'GET :id/issues/:issue_iid/closed_by' do
- let(:merge_request) do
- create(:merge_request,
- :simple,
- author: user,
- source_project: project,
- target_project: project,
- description: "closes #{issue.to_reference}")
- end
-
- before do
- create(:merge_requests_closing_issues, issue: issue, merge_request: merge_request)
- end
-
- context 'when unauthenticated' do
- it 'return public project issues' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by")
-
- expect_paginated_array_response(merge_request.id)
- end
- end
-
- it 'returns merge requests that will close issue on merge' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/closed_by", user)
-
- expect_paginated_array_response(merge_request.id)
- end
-
- context 'when no merge requests will close issue' do
- it 'returns empty array' do
- get api("/projects/#{project.id}/issues/#{closed_issue.iid}/closed_by", user)
-
- expect_paginated_array_response([])
- end
- end
-
- it "returns 404 when issue doesn't exists" do
- get api("/projects/#{project.id}/issues/9999/closed_by", user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'GET :id/issues/:issue_iid/related_merge_requests' do
- def get_related_merge_requests(project_id, issue_iid, user = nil)
- get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
- end
-
- def create_referencing_mr(user, project, issue)
- attributes = {
- author: user,
- source_project: project,
- target_project: project,
- source_branch: "master",
- target_branch: "test",
- description: "See #{issue.to_reference}"
- }
- create(:merge_request, attributes).tap do |merge_request|
- create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
- end
- end
-
- let!(:related_mr) { create_referencing_mr(user, project, issue) }
-
- context 'when unauthenticated' do
- it 'return list of referenced merge requests from issue' do
- get_related_merge_requests(project.id, issue.iid)
-
- expect_paginated_array_response(related_mr.id)
- end
-
- it 'renders 404 if project is not visible' do
- private_project = create(:project, :private)
- private_issue = create(:issue, project: private_project)
- create_referencing_mr(user, private_project, private_issue)
-
- get_related_merge_requests(private_project.id, private_issue.iid)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- it 'returns merge requests that mentioned a issue' do
- create(:merge_request,
- :simple,
- author: user,
- source_project: project,
- target_project: project,
- description: "Some description")
-
- get_related_merge_requests(project.id, issue.iid, user)
-
- expect_paginated_array_response(related_mr.id)
- end
-
- it 'returns merge requests cross-project wide' do
- project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
- merge_request = create_referencing_mr(user, project2, issue)
-
- get_related_merge_requests(project.id, issue.iid, user)
-
- expect_paginated_array_response([related_mr.id, merge_request.id])
- end
-
- it 'does not generate references to projects with no access' do
- private_project = create(:project, :private)
- create_referencing_mr(private_project.creator, private_project, issue)
-
- get_related_merge_requests(project.id, issue.iid, user)
-
- expect_paginated_array_response(related_mr.id)
- end
-
- context 'no merge request mentioned a issue' do
- it 'returns empty array' do
- get_related_merge_requests(project.id, closed_issue.iid, user)
-
- expect_paginated_array_response([])
- end
- end
-
- it "returns 404 when issue doesn't exists" do
- get_related_merge_requests(project.id, 999999, user)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe "GET /projects/:id/issues/:issue_iid/user_agent_detail" do
- let!(:user_agent_detail) { create(:user_agent_detail, subject: issue) }
-
- context 'when unauthenticated' do
- it "returns unauthorized" do
- get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail")
-
- expect(response).to have_gitlab_http_status(401)
- end
- end
-
- it 'exposes known attributes' do
- get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", admin)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['user_agent']).to eq(user_agent_detail.user_agent)
- expect(json_response['ip_address']).to eq(user_agent_detail.ip_address)
- expect(json_response['akismet_submitted']).to eq(user_agent_detail.submitted)
- end
-
- it "returns unauthorized for non-admin users" do
- get api("/projects/#{project.id}/issues/#{issue.iid}/user_agent_detail", user)
-
- expect(response).to have_gitlab_http_status(403)
- end
- end
-
- describe 'GET projects/:id/issues/:issue_iid/participants' do
- it_behaves_like 'issuable participants endpoint' do
- let(:entity) { issue }
- end
-
- it 'returns 404 if the issue is confidential' do
- post api("/projects/#{project.id}/issues/#{confidential_issue.iid}/participants", non_member)
-
- expect(response).to have_gitlab_http_status(404)
- end
- end
-end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 3defe8bbf51..89ee6f896f9 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -286,6 +286,7 @@ describe API::Jobs do
expect(json_response['ref']).to eq(job.ref)
expect(json_response['tag']).to eq(job.tag)
expect(json_response['coverage']).to eq(job.coverage)
+ expect(json_response['allow_failure']).to eq(job.allow_failure)
expect(Time.parse(json_response['created_at'])).to be_like_time(job.created_at)
expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at)
expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at)
@@ -321,6 +322,49 @@ describe API::Jobs do
end
end
+ describe 'DELETE /projects/:id/jobs/:job_id/artifacts' do
+ let!(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
+
+ before do
+ delete api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
+ end
+
+ context 'when user is anonymous' do
+ let(:api_user) { nil }
+
+ it 'does not delete artifacts' do
+ expect(job.job_artifacts.size).to eq 2
+ end
+
+ it 'returns status 401 (unauthorized)' do
+ expect(response).to have_http_status :unauthorized
+ end
+ end
+
+ context 'with developer' do
+ it 'does not delete artifacts' do
+ expect(job.job_artifacts.size).to eq 2
+ end
+
+ it 'returns status 403 (forbidden)' do
+ expect(response).to have_http_status :forbidden
+ end
+ end
+
+ context 'with authorized user' do
+ let(:maintainer) { create(:project_member, :maintainer, project: project).user }
+ let!(:api_user) { maintainer }
+
+ it 'deletes artifacts' do
+ expect(job.job_artifacts.size).to eq 0
+ end
+
+ it 'returns status 204 (no content)' do
+ expect(response).to have_http_status :no_content
+ end
+ end
+ end
+
describe 'GET /projects/:id/jobs/:job_id/artifacts/:artifact_path' do
context 'when job has artifacts' do
let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
@@ -869,8 +913,8 @@ describe API::Jobs do
expect(response).to have_gitlab_http_status(201)
expect(job.job_artifacts.count).to eq(0)
expect(job.trace.exist?).to be_falsy
- expect(job.artifacts_file.exists?).to be_falsy
- expect(job.artifacts_metadata.exists?).to be_falsy
+ expect(job.artifacts_file.present?).to be_falsy
+ expect(job.artifacts_metadata.present?).to be_falsy
expect(job.has_job_artifacts?).to be_falsy
end
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
index 3c4719964b6..f37d84fddef 100644
--- a/spec/requests/api/keys_spec.rb
+++ b/spec/requests/api/keys_spec.rb
@@ -16,7 +16,7 @@ describe API::Keys do
context 'when authenticated' do
it 'returns 404 for non-existing key' do
- get api('/keys/999999', admin)
+ get api('/keys/0', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 79edbb301f2..55f38079b1f 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -132,6 +132,19 @@ describe API::Members do
expect(json_response.map { |u| u['id'] }).to match_array [maintainer.id, developer.id, nested_user.id, project_user.id, linked_group_user.id]
end
+ it 'returns only one member for each user without returning duplicated members' do
+ linked_group.add_developer(developer)
+
+ get api("/projects/#{project.id}/members/all", developer)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.map { |u| u['id'] }).to eq [developer.id, maintainer.id, nested_user.id, project_user.id, linked_group_user.id]
+ expect(json_response.map { |u| u['access_level'] }).to eq [Gitlab::Access::DEVELOPER, Gitlab::Access::OWNER, Gitlab::Access::DEVELOPER,
+ Gitlab::Access::DEVELOPER, Gitlab::Access::DEVELOPER]
+ end
+
it 'finds all group members including inherited members' do
get api("/groups/#{nested_group.id}/members/all", developer)
@@ -236,7 +249,7 @@ describe API::Members do
params: { user_id: stranger.id, access_level: Member::REPORTER }
expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']['access_level']).to eq(["should be higher than Developer inherited membership from group #{parent.name}"])
+ expect(json_response['message']['access_level']).to eq(["should be greater than or equal to Developer inherited membership from group #{parent.name}"])
end
it 'creates the member if group level is lower', :nested_groups do
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index 6530dc956cb..8a67d98fc4c 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -30,7 +30,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
end
it 'returns a 404 when merge_request_iid not found' do
- get api("/projects/#{project.id}/merge_requests/999/versions", user)
+ get api("/projects/#{project.id}/merge_requests/0/versions", user)
expect(response).to have_gitlab_http_status(404)
end
end
@@ -53,7 +53,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
end
it 'returns a 404 when merge_request version_id is not found' do
- get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/999", user)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions/0", user)
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index b4cd3130dc5..4cb4fcc890d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -4,30 +4,408 @@ describe API::MergeRequests do
include ProjectForksHelper
let(:base_time) { Time.now }
- let(:user) { create(:user) }
- let(:admin) { create(:user, :admin) }
- let(:non_member) { create(:user) }
- let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:admin) { create(:user, :admin) }
+ let(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
- let(:pipeline) { create(:ci_empty_pipeline) }
- let(:milestone1) { create(:milestone, title: '0.9', project: project) }
- let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
- let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
+ let(:milestone1) { create(:milestone, title: '0.9', project: project) }
+ let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignees: [user], source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds, merge_commit_sha: '9999999999999999999999999999999999999999') }
+ let!(:merge_request_locked) { create(:merge_request, state: "locked", milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Locked test", created_at: base_time + 1.second) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
- let!(:label) do
- create(:label, title: 'label', color: '#FFAABB', project: project)
- end
- let!(:label2) { create(:label, title: 'a-test', color: '#FFFFFF', project: project) }
- let!(:label_link) { create(:label_link, label: label, target: merge_request) }
- let!(:label_link2) { create(:label_link, label: label2, target: merge_request) }
- let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request) }
- let!(:upvote) { create(:award_emoji, :upvote, awardable: merge_request) }
+ let(:label) { create(:label, title: 'label', color: '#FFAABB', project: project) }
+ let(:label2) { create(:label, title: 'a-test', color: '#FFFFFF', project: project) }
before do
project.add_reporter(user)
+ project.add_reporter(user2)
+
+ stub_licensed_features(multiple_merge_request_assignees: false)
+ end
+
+ shared_context 'with labels' do
+ before do
+ create(:label_link, label: label, target: merge_request)
+ create(:label_link, label: label2, target: merge_request)
+ end
+ end
+
+ shared_examples 'merge requests list' do
+ context 'when unauthenticated' do
+ it 'returns merge requests for public projects' do
+ get api(endpoint_path)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'when authenticated' do
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api(endpoint_path, user)
+ end
+
+ create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time)
+
+ merge_request = create(:merge_request, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: 'Test', created_at: base_time)
+
+ merge_request.metrics.update!(merged_by: user,
+ latest_closed_by: user,
+ latest_closed_at: 1.hour.ago,
+ merged_at: 2.hours.ago)
+
+ expect do
+ get api(endpoint_path, user)
+ end.not_to exceed_query_limit(control)
+ end
+
+ context 'with labels' do
+ include_context 'with labels'
+
+ it 'returns an array of all merge_requests' do
+ get api(endpoint_path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
+ expect(json_response.last['merge_commit_sha']).to be_nil
+ expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
+ expect(json_response.last['downvotes']).to eq(0)
+ expect(json_response.last['upvotes']).to eq(0)
+ expect(json_response.last['labels']).to eq([label2.title, label.title])
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
+ expect(json_response.first['merge_commit_sha']).not_to be_nil
+ expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
+ end
+ end
+
+ it 'returns an array of all merge_requests using simple mode' do
+ path = endpoint_path + '?view=simple'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ expect(json_response.last['iid']).to eq(merge_request.iid)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
+ expect(json_response.first['iid']).to eq(merge_request_merged.iid)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ expect(json_response.first).to have_key('web_url')
+ end
+
+ it 'returns an array of all merge_requests' do
+ path = endpoint_path + '?state'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it 'returns an array of open merge_requests' do
+ path = endpoint_path + '?state=opened'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.last['title']).to eq(merge_request.title)
+ end
+
+ it 'returns an array of closed merge_requests' do
+ path = endpoint_path + '?state=closed'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request_closed.title)
+ end
+
+ it 'returns an array of merged merge_requests' do
+ path = endpoint_path + '?state=merged'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['title']).to eq(merge_request_merged.title)
+ end
+
+ it 'matches V4 response schema' do
+ get api(endpoint_path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/merge_requests')
+ end
+
+ it 'returns an empty array if no issue matches milestone' do
+ get api(endpoint_path, user), params: { milestone: '1.0.0' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if milestone does not exist' do
+ get api(endpoint_path, user), params: { milestone: 'foo' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of merge requests in given milestone' do
+ get api(endpoint_path, user), params: { milestone: '0.9' }
+
+ closed_issues = json_response.select { |mr| mr['id'] == merge_request_closed.id }
+ expect(closed_issues.length).to eq(1)
+ expect(closed_issues.first['title']).to eq merge_request_closed.title
+ end
+
+ it 'returns an array of merge requests matching state in milestone' do
+ get api(endpoint_path, user), params: { milestone: '0.9', state: 'closed' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request_closed.id)
+ end
+
+ context 'with labels' do
+ include_context 'with labels'
+
+ it 'returns an array of labeled merge requests' do
+ path = endpoint_path + "?labels=#{label.title}"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ end
+
+ it 'returns an array of labeled merge requests where all labels match' do
+ path = endpoint_path + "?labels=#{label.title},foo,bar"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an empty array if no merge request matches labels' do
+ path = endpoint_path + '?labels=foo,bar'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(0)
+ end
+
+ it 'returns an array of labeled merge requests where all labels match' do
+ path = endpoint_path + "?labels[]=#{label.title}&labels[]=#{label2.title}"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ end
+
+ it 'returns an array of merge requests with any label when filtering by any label' do
+ get api(endpoint_path, user), params: { labels: [" #{label.title} ", " #{label2.title} "] }
+
+ expect_paginated_array_response
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+
+ it 'returns an array of merge requests with any label when filtering by any label' do
+ get api(endpoint_path, user), params: { labels: ["#{label.title} , #{label2.title}"] }
+
+ expect_paginated_array_response
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['labels']).to eq([label2.title, label.title])
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+
+ it 'returns an array of merge requests with any label when filtering by any label' do
+ get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_ANY }
+
+ expect_paginated_array_response
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+
+ it 'returns an array of merge requests without a label when filtering by no label' do
+ get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_NONE }
+
+ response_ids = json_response.map { |merge_request| merge_request['id'] }
+
+ expect_paginated_array_response
+ expect(response_ids).to contain_exactly(merge_request_closed.id, merge_request_merged.id, merge_request_locked.id)
+ end
+ end
+
+ it 'returns an array of labeled merge requests that are merged for a milestone' do
+ bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project)
+
+ mr1 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone)
+ mr2 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
+ mr3 = create(:merge_request, state: 'closed', source_project: project, target_project: project, milestone: milestone1)
+ _mr = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
+
+ create(:label_link, label: bug_label, target: mr1)
+ create(:label_link, label: bug_label, target: mr2)
+ create(:label_link, label: bug_label, target: mr3)
+
+ path = endpoint_path + "?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged"
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(mr2.id)
+ end
+
+ context 'with ordering' do
+ before do
+ @mr_later = mr_with_later_created_and_updated_at_time
+ @mr_earlier = mr_with_earlier_created_and_updated_at_time
+ end
+
+ it 'returns an array of merge_requests in ascending order' do
+ path = endpoint_path + '?sort=asc'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+
+ it 'returns an array of merge_requests in descending order' do
+ path = endpoint_path + '?sort=desc'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ context '2 merge requests with equal created_at' do
+ let!(:closed_mr2) do
+ create :merge_request,
+ state: 'closed',
+ milestone: milestone1,
+ author: user,
+ assignees: [user],
+ source_project: project,
+ target_project: project,
+ title: "Test",
+ created_at: @mr_earlier.created_at
+ end
+
+ it 'page breaks first page correctly' do
+ get api("#{endpoint_path}?sort=desc&per_page=4", user)
+
+ response_ids = json_response.map { |merge_request| merge_request['id'] }
+
+ expect(response_ids).to include(closed_mr2.id)
+ expect(response_ids).not_to include(@mr_earlier.id)
+ end
+
+ it 'page breaks second page correctly' do
+ get api("#{endpoint_path}?sort=desc&per_page=4&page=2", user)
+
+ response_ids = json_response.map { |merge_request| merge_request['id'] }
+
+ expect(response_ids).not_to include(closed_mr2.id)
+ expect(response_ids).to include(@mr_earlier.id)
+ end
+ end
+
+ it 'returns an array of merge_requests ordered by updated_at' do
+ path = endpoint_path + '?order_by=updated_at'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
+ end
+
+ it 'returns an array of merge_requests ordered by created_at' do
+ path = endpoint_path + '?order_by=created_at&sort=asc'
+
+ get api(path, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(4)
+ response_dates = json_response.map { |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
+ end
+ end
+
+ context 'source_branch param' do
+ it 'returns merge requests with the given source branch' do
+ get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' }
+
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
+ end
+ end
+
+ context 'target_branch param' do
+ it 'returns merge requests with the given target branch' do
+ get api(endpoint_path, user), params: { target_branch: merge_request_closed.target_branch, state: 'all' }
+
+ expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
+ end
+ end
+ end
end
describe 'route shadowing' do
@@ -77,7 +455,7 @@ describe API::MergeRequests do
context 'when authenticated' do
let!(:project2) { create(:project, :public, namespace: user.namespace) }
- let!(:merge_request2) { create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2) }
+ let!(:merge_request2) { create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2) }
let(:user2) { create(:user) }
it 'returns an array of all merge requests except unauthorized ones' do
@@ -120,7 +498,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests created by current user if no scope is given' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2)
@@ -128,7 +506,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests authored by the given user' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user), params: { author_id: user2.id, scope: :all }
@@ -136,7 +514,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests assigned to the given user' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all }
@@ -161,7 +539,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests assigned to me' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'assigned_to_me' }
@@ -169,7 +547,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests assigned to me (kebab-case)' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user2, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'assigned-to-me' }
@@ -177,7 +555,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests created by me' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'created_by_me' }
@@ -185,7 +563,7 @@ describe API::MergeRequests do
end
it 'returns an array of merge requests created by me (kebab-case)' do
- merge_request3 = create(:merge_request, :simple, author: user2, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user2), params: { scope: 'created-by-me' }
@@ -193,7 +571,7 @@ describe API::MergeRequests do
end
it 'returns merge requests reacted by the authenticated user by the given emoji' do
- merge_request3 = create(:merge_request, :simple, author: user, assignee: user, source_project: project2, target_project: project2, source_branch: 'other-branch')
+ merge_request3 = create(:merge_request, :simple, author: user, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
award_emoji = create(:award_emoji, awardable: merge_request3, user: user2, name: 'star')
get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' }
@@ -320,6 +698,18 @@ describe API::MergeRequests do
expect(json_response.first['title']).to eq merge_request_closed.title
expect(json_response.first['id']).to eq merge_request_closed.id
end
+
+ it 'avoids N+1 queries' do
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/projects/#{project.id}/merge_requests", user)
+ end.count
+
+ create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, created_at: base_time)
+
+ expect do
+ get api("/projects/#{project.id}/merge_requests", user)
+ end.not_to exceed_query_limit(control)
+ end
end
describe "GET /groups/:id/merge_requests" do
@@ -343,7 +733,18 @@ describe API::MergeRequests do
end
describe "GET /projects/:id/merge_requests/:merge_request_iid" do
+ it 'matches json schema' do
+ merge_request = create(:merge_request, :with_test_reports, milestone: milestone1, author: user, assignees: [user], source_project: project, target_project: project, title: "Test", created_at: base_time)
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/merge_request')
+ end
+
it 'exposes known attributes' do
+ create(:award_emoji, :downvote, awardable: merge_request)
+ create(:award_emoji, :upvote, awardable: merge_request)
+
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
expect(response).to have_gitlab_http_status(200)
@@ -393,6 +794,8 @@ describe API::MergeRequests do
end
context 'merge_request_metrics' do
+ let(:pipeline) { create(:ci_empty_pipeline) }
+
before do
merge_request.metrics.update!(merged_by: user,
latest_closed_by: user,
@@ -441,7 +844,7 @@ describe API::MergeRequests do
end
it "returns a 404 error if merge_request_iid not found" do
- get api("/projects/#{project.id}/merge_requests/999", user)
+ get api("/projects/#{project.id}/merge_requests/0", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -452,7 +855,7 @@ describe API::MergeRequests do
end
context 'Work in Progress' do
- let!(:merge_request_wip) { create(:merge_request, author: user, assignee: user, source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
+ let!(:merge_request_wip) { create(:merge_request, author: user, assignees: [user], source_project: project, target_project: project, title: "WIP: Test", created_at: base_time + 1.second) }
it "returns merge request" do
get api("/projects/#{project.id}/merge_requests/#{merge_request_wip.iid}", user)
@@ -468,7 +871,7 @@ describe API::MergeRequests do
merge_request_overflow = create(:merge_request, :simple,
author: user,
- assignee: user,
+ assignees: [user],
source_project: project,
source_branch: 'expand-collapse-files',
target_project: project,
@@ -531,7 +934,7 @@ describe API::MergeRequests do
end
it 'returns a 404 when merge_request_iid not found' do
- get api("/projects/#{project.id}/merge_requests/999/commits", user)
+ get api("/projects/#{project.id}/merge_requests/0/commits", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -551,7 +954,7 @@ describe API::MergeRequests do
end
it 'returns a 404 when merge_request_iid not found' do
- get api("/projects/#{project.id}/merge_requests/999/changes", user)
+ get api("/projects/#{project.id}/merge_requests/0/changes", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -605,26 +1008,180 @@ describe API::MergeRequests do
end
end
- describe "POST /projects/:id/merge_requests" do
- context 'between branches projects' do
- it "returns merge_request" do
- post api("/projects/#{project.id}/merge_requests", user),
- params: {
- title: 'Test merge_request',
- source_branch: 'feature_conflict',
- target_branch: 'master',
- author: user,
- labels: 'label, label2',
- milestone_id: milestone.id,
- squash: true
- }
+ describe 'POST /projects/:id/merge_requests' do
+ context 'support for deprecated assignee_id' do
+ let(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ assignee_id: user2.id
+ }
+ end
+
+ it 'creates a new merge request' do
+ post api("/projects/#{project.id}/merge_requests", user), params: params
expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('Test merge_request')
- expect(json_response['labels']).to eq(%w(label label2))
- expect(json_response['milestone']['id']).to eq(milestone.id)
- expect(json_response['squash']).to be_truthy
- expect(json_response['force_remove_source_branch']).to be_falsy
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignee']['name']).to eq(user2.name)
+ expect(json_response['assignees'].first['name']).to eq(user2.name)
+ end
+
+ it 'creates a new merge request when assignee_id is empty' do
+ params[:assignee_id] = ''
+
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignee']).to be_nil
+ end
+
+ it 'filters assignee_id of unauthorized user' do
+ private_project = create(:project, :private, :repository)
+ another_user = create(:user)
+ private_project.add_maintainer(user)
+ params[:assignee_id] = another_user.id
+
+ post api("/projects/#{private_project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['assignee']).to be_nil
+ end
+ end
+
+ context 'single assignee restrictions' do
+ let(:params) do
+ {
+ title: 'Test merge request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ assignee_ids: [user.id, user2.id]
+ }
+ end
+
+ it 'creates a new project merge request with no more than one assignee' do
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge request')
+ expect(json_response['assignees'].count).to eq(1)
+ expect(json_response['assignees'].first['name']).to eq(user.name)
+ expect(json_response.dig('assignee', 'name')).to eq(user.name)
+ end
+ end
+
+ context 'between branches projects' do
+ context 'different labels' do
+ let(:params) do
+ {
+ title: 'Test merge_request',
+ source_branch: 'feature_conflict',
+ target_branch: 'master',
+ author_id: user.id,
+ milestone_id: milestone.id,
+ squash: true
+ }
+ end
+
+ shared_examples_for 'creates merge request with labels' do
+ it 'returns merge_request' do
+ params[:labels] = labels
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['title']).to eq('Test merge_request')
+ expect(json_response['labels']).to eq(%w(label label2))
+ expect(json_response['milestone']['id']).to eq(milestone.id)
+ expect(json_response['squash']).to be_truthy
+ expect(json_response['force_remove_source_branch']).to be_falsy
+ end
+ end
+
+ it_behaves_like 'creates merge request with labels' do
+ let(:labels) { 'label, label2' }
+ end
+
+ it_behaves_like 'creates merge request with labels' do
+ let(:labels) { %w(label label2) }
+ end
+
+ it_behaves_like 'creates merge request with labels' do
+ let(:labels) { %w(label label2) }
+ end
+
+ it 'creates merge request with special label names' do
+ params[:labels] = 'label, label?, label&foo, ?, &'
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'creates merge request with special label names as array' do
+ params[:labels] = ['label', 'label?', 'label&foo, ?, &', '1, 2', 3, 4]
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ expect(json_response['labels']).to include '1'
+ expect(json_response['labels']).to include '2'
+ expect(json_response['labels']).to include '3'
+ expect(json_response['labels']).to include '4'
+ end
+
+ it 'empty label param does not add any labels' do
+ params[:labels] = ''
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['labels']).to eq([])
+ end
+
+ it 'empty label param as array does not add any labels, but only explicitly as json' do
+ params[:labels] = []
+ post api("/projects/#{project.id}/merge_requests", user),
+ params: params.to_json,
+ headers: { 'Content-Type': 'application/json' }
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['labels']).to eq([])
+ end
+
+ xit 'empty label param as array, does not add any labels' do
+ params[:labels] = []
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['labels']).to eq([])
+ end
+
+ it 'array with one empty string element does not add labels' do
+ params[:labels] = ['']
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['labels']).to eq([])
+ end
+
+ it 'array with multiple empty string elements, does not add labels' do
+ params[:labels] = ['', '', '']
+ post api("/projects/#{project.id}/merge_requests", user), params: params
+
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['labels']).to eq([])
+ end
end
it "returns 422 when source_branch equals target_branch" do
@@ -651,23 +1208,6 @@ describe API::MergeRequests do
expect(response).to have_gitlab_http_status(400)
end
- it 'allows special label names' do
- post api("/projects/#{project.id}/merge_requests", user),
- params: {
- title: 'Test merge_request',
- source_branch: 'markdown',
- target_branch: 'master',
- author: user,
- labels: 'label, label?, label&foo, ?, &'
- }
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
- end
-
context 'with existing MR' do
before do
post api("/projects/#{project.id}/merge_requests", user),
@@ -890,7 +1430,12 @@ describe API::MergeRequests do
end
it 'returns 405 if the build failed for a merge request that requires success' do
- allow_any_instance_of(MergeRequest).to receive(:mergeable_ci_state?).and_return(false)
+ project.update!(only_allow_merge_if_pipeline_succeeds: true)
+
+ create(:ci_pipeline,
+ :failed,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
@@ -928,7 +1473,7 @@ describe API::MergeRequests do
end
it "enables merge when pipeline succeeds if the pipeline is active" do
- allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow_any_instance_of(MergeRequest).to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(pipeline).to receive(:active?).and_return(true)
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user), params: { merge_when_pipeline_succeeds: true }
@@ -939,7 +1484,7 @@ describe API::MergeRequests do
end
it "enables merge when pipeline succeeds if the pipeline is active and only_allow_merge_if_pipeline_succeeds is true" do
- allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(pipeline)
+ allow_any_instance_of(MergeRequest).to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(pipeline).to receive(:active?).and_return(true)
project.update_attribute(:only_allow_merge_if_pipeline_succeeds, true)
@@ -1001,7 +1546,95 @@ describe API::MergeRequests do
end
end
+ describe "GET /projects/:id/merge_requests/:merge_request_iid/merge_ref" do
+ before do
+ merge_request.mark_as_unchecked!
+ end
+
+ let(:merge_request_iid) { merge_request.iid }
+
+ let(:url) do
+ "/projects/#{project.id}/merge_requests/#{merge_request_iid}/merge_ref"
+ end
+
+ it 'returns the generated ID from the merge service in case of success' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['commit_id']).to eq(merge_request.merge_ref_head.sha)
+ end
+
+ it "returns 400 if branch can't be merged" do
+ merge_request.update!(merge_status: 'cannot_be_merged')
+
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(400)
+ expect(json_response['message']).to eq('Merge request is not mergeable')
+ end
+
+ context 'when user has no access to the MR' do
+ let(:project) { create(:project, :private) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ it 'returns 404' do
+ project.add_guest(user)
+
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+ end
+
+ context 'when invalid merge request IID' do
+ let(:merge_request_iid) { '12345' }
+
+ it 'returns 404' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when merge request ID is used instead IID' do
+ let(:merge_request_iid) { merge_request.id }
+
+ it 'returns 404' do
+ get api(url, user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
describe "PUT /projects/:id/merge_requests/:merge_request_iid" do
+ context 'updates force_remove_source_branch properly' do
+ it 'sets to false' do
+ merge_request.update(merge_params: { 'force_remove_source_branch' => true } )
+
+ expect(merge_request.force_remove_source_branch?).to be_truthy
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: { state_event: "close", remove_source_branch: false }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['state']).to eq('closed')
+ expect(json_response['force_remove_source_branch']).to be_falsey
+ end
+
+ it 'sets to true' do
+ merge_request.update(merge_params: { 'force_remove_source_branch' => false } )
+
+ expect(merge_request.force_remove_source_branch?).to be_falsey
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: { state_event: "close", remove_source_branch: true }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['state']).to eq('closed')
+ expect(json_response['force_remove_source_branch']).to be_truthy
+ end
+ end
+
context "to close a MR" do
it "returns merge_request" do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: { state_event: "close" }
@@ -1049,19 +1682,110 @@ describe API::MergeRequests do
expect(json_response['force_remove_source_branch']).to be_truthy
end
- it 'allows special label names' do
- put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
- params: {
- title: 'new issue',
- labels: 'label, label?, label&foo, ?, &'
- }
+ it 'filters assignee_id of unauthorized user' do
+ private_project = create(:project, :private, :repository)
+ mr = create(:merge_request, source_project: private_project, target_project: private_project)
+ another_user = create(:user)
+ private_project.add_maintainer(user)
+ params = { assignee_id: another_user.id }
+
+ put api("/projects/#{private_project.id}/merge_requests/#{mr.iid}", user), params: params
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['assignee']).to be_nil
+ end
+
+ context 'when updating labels' do
+ it 'allows special label names' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
+ params: {
+ title: 'new issue',
+ labels: 'label, label?, label&foo, ?, &'
+ }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ end
+
+ it 'also accepts labels as an array' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
+ params: {
+ title: 'new issue',
+ labels: ['label', 'label?', 'label&foo, ?, &', '1, 2', 3, 4]
+ }
- expect(response.status).to eq(200)
- expect(json_response['labels']).to include 'label'
- expect(json_response['labels']).to include 'label?'
- expect(json_response['labels']).to include 'label&foo'
- expect(json_response['labels']).to include '?'
- expect(json_response['labels']).to include '&'
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to include 'label'
+ expect(json_response['labels']).to include 'label?'
+ expect(json_response['labels']).to include 'label&foo'
+ expect(json_response['labels']).to include '?'
+ expect(json_response['labels']).to include '&'
+ expect(json_response['labels']).to include '1'
+ expect(json_response['labels']).to include '2'
+ expect(json_response['labels']).to include '3'
+ expect(json_response['labels']).to include '4'
+ end
+
+ it 'empty label param removes labels' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
+ params: {
+ title: 'new issue',
+ labels: ''
+ }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to eq []
+ end
+
+ it 'label param as empty array, but only explicitly as json, removes labels' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
+ params: {
+ title: 'new issue',
+ labels: []
+ }.to_json,
+ headers: { 'Content-Type' => 'application/json' }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to eq []
+ end
+
+ xit 'empty label as array, removes labels' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
+ params: {
+ title: 'new issue',
+ labels: []
+ }
+
+ expect(response.status).to eq(200)
+ # fails, as grape ommits for some reason empty array as optional param value, so nothing it passed along
+ expect(json_response['labels']).to eq []
+ end
+
+ it 'array with one empty string element removes labels' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
+ params: {
+ title: 'new issue',
+ labels: ['']
+ }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to eq []
+ end
+
+ it 'array with multiple empty string elements, removes labels' do
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
+ params: {
+ title: 'new issue',
+ labels: ['', '', '']
+ }
+
+ expect(response.status).to eq(200)
+ expect(json_response['labels']).to eq []
+ end
end
it 'does not update state when title is empty' do
@@ -1125,7 +1849,7 @@ describe API::MergeRequests do
issue = create(:issue, project: jira_project)
description = "Closes #{ext_issue.to_reference(jira_project)}\ncloses #{issue.to_reference}"
merge_request = create(:merge_request,
- :simple, author: user, assignee: user, source_project: jira_project, description: description)
+ :simple, author: user, assignees: [user], source_project: jira_project, description: description)
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
@@ -1239,7 +1963,7 @@ describe API::MergeRequests do
describe 'POST :id/merge_requests/:merge_request_iid/cancel_merge_when_pipeline_succeeds' do
before do
- ::MergeRequests::MergeWhenPipelineSucceedsService.new(merge_request.target_project, user).execute(merge_request)
+ ::AutoMergeService.new(merge_request.target_project, user).execute(merge_request, AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
end
it 'removes the merge_when_pipeline_succeeds status' do
diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb
index 145356c4df5..2e376109b42 100644
--- a/spec/requests/api/namespaces_spec.rb
+++ b/spec/requests/api/namespaces_spec.rb
@@ -149,7 +149,7 @@ describe API::Namespaces do
context "when namespace doesn't exist" do
it 'returns not-found' do
- get api('/namespaces/9999', request_actor)
+ get api('/namespaces/0', request_actor)
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/pipeline_schedules_spec.rb b/spec/requests/api/pipeline_schedules_spec.rb
index 870ef34437f..072bd02f2ac 100644
--- a/spec/requests/api/pipeline_schedules_spec.rb
+++ b/spec/requests/api/pipeline_schedules_spec.rb
@@ -91,6 +91,7 @@ describe API::PipelineSchedules do
let(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: developer) }
before do
+ pipeline_schedule.variables << build(:ci_pipeline_schedule_variable)
pipeline_schedule.pipelines << build(:ci_pipeline, project: project)
end
@@ -331,13 +332,14 @@ describe API::PipelineSchedules do
it 'creates pipeline_schedule_variable' do
expect do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer),
- params: params
+ params: params.merge(variable_type: 'file')
end.to change { pipeline_schedule.variables.count }.by(1)
expect(response).to have_gitlab_http_status(:created)
expect(response).to match_response_schema('pipeline_schedule_variable')
expect(json_response['key']).to eq(params[:key])
expect(json_response['value']).to eq(params[:value])
+ expect(json_response['variable_type']).to eq('file')
end
end
@@ -389,11 +391,12 @@ describe API::PipelineSchedules do
context 'authenticated user with valid permissions' do
it 'updates pipeline_schedule_variable' do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer),
- params: { value: 'updated_value' }
+ params: { value: 'updated_value', variable_type: 'file' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('pipeline_schedule_variable')
expect(json_response['value']).to eq('updated_value')
+ expect(json_response['variable_type']).to eq('file')
end
end
diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb
index 52599db9a9e..35b3dd219f7 100644
--- a/spec/requests/api/pipelines_spec.rb
+++ b/spec/requests/api/pipelines_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe API::Pipelines do
@@ -292,6 +294,7 @@ describe API::Pipelines do
expect(variable.key).to eq(expected_variable['key'])
expect(variable.value).to eq(expected_variable['value'])
+ expect(variable.variable_type).to eq(expected_variable['variable_type'])
end
end
@@ -312,7 +315,7 @@ describe API::Pipelines do
end
context 'variables given' do
- let(:variables) { [{ 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] }
+ let(:variables) { [{ 'variable_type' => 'file', 'key' => 'UPLOAD_TO_S3', 'value' => 'true' }] }
it 'creates and returns a new pipeline using the given variables' do
expect do
@@ -328,7 +331,7 @@ describe API::Pipelines do
end
describe 'using variables conditions' do
- let(:variables) { [{ 'key' => 'STAGING', 'value' => 'true' }] }
+ let(:variables) { [{ 'variable_type' => 'env_var', 'key' => 'STAGING', 'value' => 'true' }] }
before do
config = YAML.dump(test: { script: 'test', only: { variables: ['$STAGING'] } })
@@ -399,6 +402,13 @@ describe API::Pipelines do
describe 'GET /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
+ it 'exposes known attributes' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to match_response_schema('public_api/v4/pipeline/detail')
+ end
+
it 'returns project pipelines' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", user)
@@ -428,7 +438,7 @@ describe API::Pipelines do
end
context 'unauthorized user' do
- it 'should not return a project pipeline' do
+ it 'does not return a project pipeline' do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(404)
@@ -438,6 +448,72 @@ describe API::Pipelines do
end
end
+ describe 'GET /projects/:id/pipelines/:pipeline_id/variables' do
+ subject { get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", api_user) }
+
+ let(:api_user) { user }
+
+ context 'user is a mantainer' do
+ it 'returns pipeline variables empty' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_empty
+ end
+
+ context 'with variables' do
+ let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') }
+
+ it 'returns pipeline variables' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" })
+ end
+ end
+ end
+
+ context 'user is a developer' do
+ let(:pipeline_owner_user) { create(:user) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, user: pipeline_owner_user) }
+
+ before do
+ project.add_developer(api_user)
+ end
+
+ context 'pipeline created by the developer user' do
+ let(:api_user) { pipeline_owner_user }
+ let!(:variable) { create(:ci_pipeline_variable, pipeline: pipeline, key: 'foo', value: 'bar') }
+
+ it 'returns pipeline variables' do
+ subject
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to contain_exactly({ "variable_type" => "env_var", "key" => "foo", "value" => "bar" })
+ end
+ end
+
+ context 'pipeline created is not created by the developer user' do
+ let(:api_user) { create(:user) }
+
+ it 'does not return pipeline variables' do
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'user is not a project member' do
+ it 'does not return pipeline variables' do
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/variables", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ expect(json_response['message']).to eq '404 Project Not Found'
+ end
+ end
+ end
+
describe 'DELETE /projects/:id/pipelines/:pipeline_id' do
context 'authorized user' do
let(:owner) { project.owner }
@@ -474,7 +550,7 @@ describe API::Pipelines do
context 'unauthorized user' do
context 'when user is not member' do
- it 'should return a 404' do
+ it 'returns a 404' do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", non_member)
expect(response).to have_gitlab_http_status(404)
@@ -489,7 +565,7 @@ describe API::Pipelines do
project.add_developer(developer)
end
- it 'should return a 403' do
+ it 'returns a 403' do
delete api("/projects/#{project.id}/pipelines/#{pipeline.id}", developer)
expect(response).to have_gitlab_http_status(403)
@@ -519,7 +595,7 @@ describe API::Pipelines do
end
context 'unauthorized user' do
- it 'should not return a project pipeline' do
+ it 'does not return a project pipeline' do
post api("/projects/#{project.id}/pipelines/#{pipeline.id}/retry", non_member)
expect(response).to have_gitlab_http_status(404)
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 9bab1f95150..fc0381159dd 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -22,7 +22,7 @@ describe API::ProjectClusters do
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
get api("/projects/#{project.id}/clusters", non_member)
expect(response).to have_gitlab_http_status(404)
@@ -34,15 +34,15 @@ describe API::ProjectClusters do
get api("/projects/#{project.id}/clusters", current_user)
end
- it 'should respond with 200' do
+ it 'responds with 200' do
expect(response).to have_gitlab_http_status(200)
end
- it 'should include pagination headers' do
+ it 'includes pagination headers' do
expect(response).to include_pagination_headers
end
- it 'should only include authorized clusters' do
+ it 'onlies include authorized clusters' do
cluster_ids = json_response.map { |cluster| cluster['id'] }
expect(cluster_ids).to match_array(clusters.pluck(:id))
@@ -60,14 +60,14 @@ describe API::ProjectClusters do
end
let(:cluster) do
- create(:cluster, :project, :provided_by_gcp,
+ create(:cluster, :project, :provided_by_gcp, :with_domain,
platform_kubernetes: platform_kubernetes,
user: current_user,
projects: [project])
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
get api("/projects/#{project.id}/clusters/#{cluster_id}", non_member)
expect(response).to have_gitlab_http_status(404)
@@ -88,6 +88,7 @@ describe API::ProjectClusters do
expect(json_response['platform_type']).to eq('kubernetes')
expect(json_response['environment_scope']).to eq('*')
expect(json_response['cluster_type']).to eq('project_type')
+ expect(json_response['domain']).to eq('example.com')
end
it 'returns project information' do
@@ -131,7 +132,7 @@ describe API::ProjectClusters do
projects: [project])
end
- it 'should not include GCP provider info' do
+ it 'does not include GCP provider info' do
expect(json_response['provider_gcp']).not_to be_present
end
end
@@ -187,12 +188,14 @@ describe API::ProjectClusters do
let(:cluster_params) do
{
name: 'test-cluster',
+ domain: 'domain.example.com',
+ managed: false,
platform_kubernetes_attributes: platform_kubernetes_attributes
}
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
post api("/projects/#{project.id}/clusters/user", non_member), params: cluster_params
expect(response).to have_gitlab_http_status(404)
@@ -205,11 +208,11 @@ describe API::ProjectClusters do
end
context 'with valid params' do
- it 'should respond with 201' do
+ it 'responds with 201' do
expect(response).to have_gitlab_http_status(201)
end
- it 'should create a new Cluster::Cluster' do
+ it 'creates a new Cluster::Cluster' do
cluster_result = Clusters::Cluster.find(json_response["id"])
platform_kubernetes = cluster_result.platform
@@ -217,6 +220,8 @@ describe API::ProjectClusters do
expect(cluster_result).to be_kubernetes
expect(cluster_result.project).to eq(project)
expect(cluster_result.name).to eq('test-cluster')
+ expect(cluster_result.domain).to eq('domain.example.com')
+ expect(cluster_result.managed).to be_falsy
expect(platform_kubernetes.rbac?).to be_truthy
expect(platform_kubernetes.api_url).to eq(api_url)
expect(platform_kubernetes.namespace).to eq(namespace)
@@ -243,7 +248,7 @@ describe API::ProjectClusters do
context 'when user sets authorization type as ABAC' do
let(:authorization_type) { 'abac' }
- it 'should create an ABAC cluster' do
+ it 'creates an ABAC cluster' do
cluster_result = Clusters::Cluster.find(json_response['id'])
expect(cluster_result.platform.abac?).to be_truthy
@@ -253,15 +258,15 @@ describe API::ProjectClusters do
context 'with invalid params' do
let(:namespace) { 'invalid_namespace' }
- it 'should respond with 400' do
+ it 'responds with 400' do
expect(response).to have_gitlab_http_status(400)
end
- it 'should not create a new Clusters::Cluster' do
+ it 'does not create a new Clusters::Cluster' do
expect(project.reload.clusters).to be_empty
end
- it 'should return validation errors' do
+ it 'returns validation errors' do
expect(json_response['message']['platform_kubernetes.namespace'].first).to be_present
end
end
@@ -275,11 +280,11 @@ describe API::ProjectClusters do
post api("/projects/#{project.id}/clusters/user", current_user), params: cluster_params
end
- it 'should respond with 403' do
+ it 'responds with 403' do
expect(response).to have_gitlab_http_status(403)
end
- it 'should return an appropriate message' do
+ it 'returns an appropriate message' do
expect(json_response['message']).to include('Instance does not support multiple Kubernetes clusters')
end
end
@@ -294,6 +299,7 @@ describe API::ProjectClusters do
let(:update_params) do
{
+ domain: 'new-domain.com',
platform_kubernetes_attributes: platform_kubernetes_attributes
}
end
@@ -310,7 +316,7 @@ describe API::ProjectClusters do
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
put api("/projects/#{project.id}/clusters/#{cluster.id}", non_member), params: update_params
expect(response).to have_gitlab_http_status(404)
@@ -325,29 +331,30 @@ describe API::ProjectClusters do
end
context 'with valid params' do
- it 'should respond with 200' do
+ it 'responds with 200' do
expect(response).to have_gitlab_http_status(200)
end
- it 'should update cluster attributes' do
+ it 'updates cluster attributes' do
+ expect(cluster.domain).to eq('new-domain.com')
expect(cluster.platform_kubernetes.namespace).to eq('new-namespace')
- expect(cluster.kubernetes_namespace.namespace).to eq('new-namespace')
end
end
context 'with invalid params' do
let(:namespace) { 'invalid_namespace' }
- it 'should respond with 400' do
+ it 'responds with 400' do
expect(response).to have_gitlab_http_status(400)
end
- it 'should not update cluster attributes' do
+ it 'does not update cluster attributes' do
+ expect(cluster.domain).not_to eq('new_domain.com')
expect(cluster.platform_kubernetes.namespace).not_to eq('invalid_namespace')
- expect(cluster.kubernetes_namespace.namespace).not_to eq('invalid_namespace')
+ expect(cluster.kubernetes_namespace_for(project)).not_to eq('invalid_namespace')
end
- it 'should return validation errors' do
+ it 'returns validation errors' do
expect(json_response['message']['platform_kubernetes.namespace'].first).to match('can contain only lowercase letters')
end
end
@@ -361,11 +368,11 @@ describe API::ProjectClusters do
}
end
- it 'should respond with 400' do
+ it 'responds with 400' do
expect(response).to have_gitlab_http_status(400)
end
- it 'should return validation error' do
+ it 'returns validation error' do
expect(json_response['message']['platform_kubernetes.base'].first).to eq('Cannot modify managed Kubernetes cluster')
end
end
@@ -373,7 +380,7 @@ describe API::ProjectClusters do
context 'when user tries to change namespace' do
let(:namespace) { 'new-namespace' }
- it 'should respond with 200' do
+ it 'responds with 200' do
expect(response).to have_gitlab_http_status(200)
end
end
@@ -402,11 +409,11 @@ describe API::ProjectClusters do
}
end
- it 'should respond with 200' do
+ it 'responds with 200' do
expect(response).to have_gitlab_http_status(200)
end
- it 'should update platform kubernetes attributes' do
+ it 'updates platform kubernetes attributes' do
platform_kubernetes = cluster.platform_kubernetes
expect(cluster.name).to eq('new-name')
@@ -419,7 +426,7 @@ describe API::ProjectClusters do
context 'with a cluster that does not belong to user' do
let(:cluster) { create(:cluster, :project, :provided_by_user) }
- it 'should respond with 404' do
+ it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
@@ -435,7 +442,7 @@ describe API::ProjectClusters do
end
context 'non-authorized user' do
- it 'should respond with 404' do
+ it 'responds with 404' do
delete api("/projects/#{project.id}/clusters/#{cluster.id}", non_member), params: cluster_params
expect(response).to have_gitlab_http_status(404)
@@ -447,18 +454,18 @@ describe API::ProjectClusters do
delete api("/projects/#{project.id}/clusters/#{cluster.id}", current_user), params: cluster_params
end
- it 'should respond with 204' do
+ it 'responds with 204' do
expect(response).to have_gitlab_http_status(204)
end
- it 'should delete the cluster' do
+ it 'deletes the cluster' do
expect(Clusters::Cluster.exists?(id: cluster.id)).to be_falsy
end
context 'with a cluster that does not belong to user' do
let(:cluster) { create(:cluster, :project, :provided_by_user) }
- it 'should respond with 404' do
+ it 'responds with 404' do
expect(response).to have_gitlab_http_status(404)
end
end
diff --git a/spec/requests/api/project_events_spec.rb b/spec/requests/api/project_events_spec.rb
new file mode 100644
index 00000000000..43df9993eb9
--- /dev/null
+++ b/spec/requests/api/project_events_spec.rb
@@ -0,0 +1,156 @@
+require 'spec_helper'
+
+describe API::ProjectEvents do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
+ let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
+ let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
+
+ describe 'GET /projects/:id/events' do
+ context 'when unauthenticated ' do
+ it 'returns 404 for private project' do
+ get api("/projects/#{private_project.id}/events")
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 200 status for a public project' do
+ public_project = create(:project, :public)
+
+ get api("/projects/#{public_project.id}/events")
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ context 'with inaccessible events' do
+ let(:public_project) { create(:project, :public, creator_id: user.id, namespace: user.namespace) }
+ let(:confidential_issue) { create(:closed_issue, confidential: true, project: public_project, author: user) }
+ let!(:confidential_event) { create(:event, project: public_project, author: user, target: confidential_issue, action: Event::CLOSED) }
+ let(:public_issue) { create(:closed_issue, project: public_project, author: user) }
+ let!(:public_event) { create(:event, project: public_project, author: user, target: public_issue, action: Event::CLOSED) }
+
+ it 'returns only accessible events' do
+ get api("/projects/#{public_project.id}/events", non_member)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns all events when the user has access' do
+ get api("/projects/#{public_project.id}/events", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(2)
+ end
+ end
+
+ context 'pagination' do
+ let(:public_project) { create(:project, :public) }
+
+ before do
+ create(:event,
+ project: public_project,
+ target: create(:issue, project: public_project, title: 'Issue 1'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-10'))
+ create(:event,
+ project: public_project,
+ target: create(:issue, confidential: true, project: public_project, title: 'Confidential event'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-11'))
+ create(:event,
+ project: public_project,
+ target: create(:issue, project: public_project, title: 'Issue 2'),
+ action: Event::CLOSED,
+ created_at: Date.parse('2018-12-12'))
+ end
+
+ it 'correctly returns the second page without inaccessible events' do
+ get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 2 }
+
+ titles = json_response.map { |event| event['target_title'] }
+
+ expect(titles.first).to eq('Issue 1')
+ expect(titles).not_to include('Confidential event')
+ end
+
+ it 'correctly returns the first page without inaccessible events' do
+ get api("/projects/#{public_project.id}/events", user), params: { per_page: 2, page: 1 }
+
+ titles = json_response.map { |event| event['target_title'] }
+
+ expect(titles.first).to eq('Issue 2')
+ expect(titles).not_to include('Confidential event')
+ end
+ end
+
+ context 'when not permitted to read' do
+ it 'returns 404' do
+ get api("/projects/#{private_project.id}/events", non_member)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'returns project events' do
+ get api("/projects/#{private_project.id}/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+
+ it 'returns 404 if project does not exist' do
+ get api("/projects/1234/events", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ context 'when the requesting token does not have "api" scope' do
+ let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
+
+ it 'returns a "403" response' do
+ get api("/projects/#{private_project.id}/events", personal_access_token: token)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ context 'when exists some events' do
+ let(:merge_request1) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+ let(:merge_request2) { create(:merge_request, :closed, author: user, assignees: [user], source_project: private_project, title: 'Test') }
+
+ before do
+ create_event(merge_request1)
+ end
+
+ it 'avoids N+1 queries' do
+ control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do
+ get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
+ end.count
+
+ create_event(merge_request2)
+
+ expect do
+ get api("/projects/#{private_project.id}/events", user), params: { target_type: :merge_request }
+ end.not_to exceed_all_query_limit(control_count)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response.size).to eq(2)
+ expect(json_response.map { |r| r['target_id'] }).to match_array([merge_request1.id, merge_request2.id])
+ end
+
+ def create_event(target)
+ create(:event, project: private_project, author: user, target: target)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
index 49b5dfb0b33..895f05a98e8 100644
--- a/spec/requests/api/project_milestones_spec.rb
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -23,13 +23,13 @@ describe API::ProjectMilestones do
end
it 'returns 404 response when the project does not exists' do
- delete api("/projects/999/milestones/#{milestone.id}", user)
+ delete api("/projects/0/milestones/#{milestone.id}", user)
expect(response).to have_gitlab_http_status(404)
end
it 'returns 404 response when the milestone does not exists' do
- delete api("/projects/#{project.id}/milestones/999", user)
+ delete api("/projects/#{project.id}/milestones/0", user)
expect(response).to have_gitlab_http_status(404)
end
@@ -49,4 +49,74 @@ describe API::ProjectMilestones do
params: { state_event: 'close' }
end
end
+
+ describe 'POST /projects/:id/milestones/:milestone_id/promote' do
+ let(:group) { create(:group) }
+
+ before do
+ project.update(namespace: group)
+ end
+
+ context 'when user does not have permission to promote milestone' do
+ before do
+ group.add_guest(user)
+ end
+
+ it 'returns 403' do
+ post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+
+ context 'when user has permission' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns 200' do
+ post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(group.milestones.first.title).to eq(milestone.title)
+ end
+
+ it 'returns 200 for closed milestone' do
+ post api("/projects/#{project.id}/milestones/#{closed_milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(group.milestones.first.title).to eq(closed_milestone.title)
+ end
+ end
+
+ context 'when no such resources' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'returns 404 response when the project does not exist' do
+ post api("/projects/0/milestones/#{milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns 404 response when the milestone does not exist' do
+ post api("/projects/#{project.id}/milestones/0/promote", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'when project does not belong to group' do
+ before do
+ project.update(namespace: user.namespace)
+ end
+
+ it 'returns 403' do
+ post api("/projects/#{project.id}/milestones/#{milestone.id}/promote", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/project_statistics_spec.rb b/spec/requests/api/project_statistics_spec.rb
new file mode 100644
index 00000000000..184d0a72c37
--- /dev/null
+++ b/spec/requests/api/project_statistics_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::ProjectStatistics do
+ let(:maintainer) { create(:user) }
+ let(:public_project) { create(:project, :public) }
+
+ before do
+ public_project.add_maintainer(maintainer)
+ end
+
+ describe 'GET /projects/:id/statistics' do
+ let!(:fetch_statistics1) { create(:project_daily_statistic, project: public_project, fetch_count: 30, date: 29.days.ago) }
+ let!(:fetch_statistics2) { create(:project_daily_statistic, project: public_project, fetch_count: 4, date: 3.days.ago) }
+ let!(:fetch_statistics3) { create(:project_daily_statistic, project: public_project, fetch_count: 3, date: 2.days.ago) }
+ let!(:fetch_statistics4) { create(:project_daily_statistic, project: public_project, fetch_count: 2, date: 1.day.ago) }
+ let!(:fetch_statistics5) { create(:project_daily_statistic, project: public_project, fetch_count: 1, date: Date.today) }
+ let!(:fetch_statistics_other_project) { create(:project_daily_statistic, project: create(:project), fetch_count: 29, date: 29.days.ago) }
+
+ it 'returns the fetch statistics of the last 30 days' do
+ get api("/projects/#{public_project.id}/statistics", maintainer)
+
+ expect(response).to have_gitlab_http_status(200)
+ fetches = json_response['fetches']
+ expect(fetches['total']).to eq(40)
+ expect(fetches['days'].length).to eq(5)
+ expect(fetches['days'].first).to eq({ 'count' => fetch_statistics5.fetch_count, 'date' => fetch_statistics5.date.to_s })
+ expect(fetches['days'].last).to eq({ 'count' => fetch_statistics1.fetch_count, 'date' => fetch_statistics1.date.to_s })
+ end
+
+ it 'excludes the fetch statistics older than 30 days' do
+ create(:project_daily_statistic, fetch_count: 31, project: public_project, date: 30.days.ago)
+
+ get api("/projects/#{public_project.id}/statistics", maintainer)
+
+ expect(response).to have_gitlab_http_status(200)
+ fetches = json_response['fetches']
+ expect(fetches['total']).to eq(40)
+ expect(fetches['days'].length).to eq(5)
+ expect(fetches['days'].last).to eq({ 'count' => fetch_statistics1.fetch_count, 'date' => fetch_statistics1.date.to_s })
+ end
+
+ it 'responds with 403 when the user is not a maintainer of the repository' do
+ developer = create(:user)
+ public_project.add_developer(developer)
+
+ get api("/projects/#{public_project.id}/statistics", developer)
+
+ expect(response).to have_gitlab_http_status(403)
+ expect(json_response['message']).to eq('403 Forbidden')
+ end
+
+ it 'responds with 404 when daily_statistics_enabled? is false' do
+ stub_feature_flags(project_daily_statistics: { thing: public_project, enabled: false })
+
+ get api("/projects/#{public_project.id}/statistics", maintainer)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 792abdb2972..799e84e83c1 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -13,12 +13,18 @@ shared_examples 'languages and percentages JSON response' do
)
end
- it 'returns expected language values' do
- get api("/projects/#{project.id}/languages", user)
+ context "when the languages haven't been detected yet" do
+ it 'returns expected language values' do
+ get api("/projects/#{project.id}/languages", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq({})
+
+ get api("/projects/#{project.id}/languages", user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to eq(expected_languages)
- expect(json_response.count).to be > 1
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(JSON.parse(response.body)).to eq(expected_languages)
+ end
end
context 'when the languages were detected before' do
@@ -40,6 +46,8 @@ shared_examples 'languages and percentages JSON response' do
end
describe API::Projects do
+ include ExternalAuthorizationServiceHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
@@ -136,6 +144,7 @@ describe API::Projects do
end
let!(:public_project) { create(:project, :public, name: 'public_project') }
+
before do
project
project2
@@ -495,8 +504,9 @@ describe API::Projects do
project4.add_reporter(user2)
end
- it 'returns an array of groups the user has at least developer access' do
+ it 'returns an array of projects the user has at least developer access' do
get api('/projects', user2), params: { min_access_level: 30 }
+
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
@@ -778,7 +788,7 @@ describe API::Projects do
let!(:public_project) { create(:project, :public, name: 'public_project', creator_id: user4.id, namespace: user4.namespace) }
it 'returns error when user not found' do
- get api('/users/9999/projects/')
+ get api('/users/0/projects/')
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -968,8 +978,16 @@ describe API::Projects do
describe 'GET /projects/:id' do
context 'when unauthenticated' do
- it 'returns the public projects' do
- public_project = create(:project, :public)
+ it 'does not return private projects' do
+ private_project = create(:project, :private)
+
+ get api("/projects/#{private_project.id}")
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'returns public projects' do
+ public_project = create(:project, :repository, :public)
get api("/projects/#{public_project.id}")
@@ -977,11 +995,84 @@ describe API::Projects do
expect(json_response['id']).to eq(public_project.id)
expect(json_response['description']).to eq(public_project.description)
expect(json_response['default_branch']).to eq(public_project.default_branch)
+ expect(json_response['ci_config_path']).to eq(public_project.ci_config_path)
expect(json_response.keys).not_to include('permissions')
end
+
+ context 'and the project has a private repository' do
+ let(:project) { create(:project, :repository, :public, :repository_private) }
+ let(:protected_attributes) { %w(default_branch ci_config_path) }
+
+ it 'hides protected attributes of private repositories if user is not a member' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ protected_attributes.each do |attribute|
+ expect(json_response.keys).not_to include(attribute)
+ end
+ end
+
+ it 'exposes protected attributes of private repositories if user is a member' do
+ project.add_developer(user)
+
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ protected_attributes.each do |attribute|
+ expect(json_response.keys).to include(attribute)
+ end
+ end
+ end
end
- context 'when authenticated' do
+ context 'when authenticated as an admin' do
+ it 'returns a project by id' do
+ project
+ project_member
+ group = create(:group)
+ link = create(:project_group_link, project: project, group: group)
+
+ get api("/projects/#{project.id}", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(project.id)
+ expect(json_response['description']).to eq(project.description)
+ expect(json_response['default_branch']).to eq(project.default_branch)
+ expect(json_response['tag_list']).to be_an Array
+ expect(json_response['archived']).to be_falsey
+ expect(json_response['visibility']).to be_present
+ expect(json_response['ssh_url_to_repo']).to be_present
+ expect(json_response['http_url_to_repo']).to be_present
+ expect(json_response['web_url']).to be_present
+ expect(json_response['owner']).to be_a Hash
+ expect(json_response['name']).to eq(project.name)
+ expect(json_response['path']).to be_present
+ expect(json_response['issues_enabled']).to be_present
+ expect(json_response['merge_requests_enabled']).to be_present
+ expect(json_response['wiki_enabled']).to be_present
+ expect(json_response['jobs_enabled']).to be_present
+ expect(json_response['snippets_enabled']).to be_present
+ expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['created_at']).to be_present
+ expect(json_response['last_activity_at']).to be_present
+ expect(json_response['shared_runners_enabled']).to be_present
+ expect(json_response['creator_id']).to be_present
+ expect(json_response['namespace']).to be_present
+ expect(json_response['avatar_url']).to be_nil
+ expect(json_response['star_count']).to be_present
+ expect(json_response['forks_count']).to be_present
+ expect(json_response['public_jobs']).to be_present
+ expect(json_response['shared_with_groups']).to be_an Array
+ expect(json_response['shared_with_groups'].length).to eq(1)
+ expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
+ expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
+ expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
+ expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
+ expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
+ end
+ end
+
+ context 'when authenticated as a regular user' do
before do
project
project_member
@@ -1004,7 +1095,6 @@ describe API::Projects do
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
expect(json_response['owner']).to be_a Hash
- expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to be_present
expect(json_response['issues_enabled']).to be_present
@@ -1092,7 +1182,9 @@ describe API::Projects do
'path' => user.namespace.path,
'kind' => user.namespace.kind,
'full_path' => user.namespace.full_path,
- 'parent_id' => nil
+ 'parent_id' => nil,
+ 'avatar_url' => user.avatar_url,
+ 'web_url' => Gitlab::Routing.url_helpers.user_url(user)
})
end
@@ -1130,6 +1222,36 @@ describe API::Projects do
expect(json_response).to include 'statistics'
end
+ context "and the project has a private repository" do
+ let(:project) { create(:project, :public, :repository, :repository_private) }
+
+ it "does not include statistics if user is not a member" do
+ get api("/projects/#{project.id}", user), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).not_to include 'statistics'
+ end
+
+ it "includes statistics if user is a member" do
+ project.add_developer(user)
+
+ get api("/projects/#{project.id}", user), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to include 'statistics'
+ end
+
+ it "includes statistics also when repository is disabled" do
+ project.add_developer(user)
+ project.project_feature.update_attribute(:repository_access_level, ProjectFeature::DISABLED)
+
+ get api("/projects/#{project.id}", user), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to include 'statistics'
+ end
+ end
+
it "includes import_error if user can admin project" do
get api("/projects/#{project.id}", user)
@@ -1264,6 +1386,70 @@ describe API::Projects do
end
end
end
+
+ context 'when project belongs to a group namespace' do
+ let(:group) { create(:group, :with_avatar) }
+ let(:project) { create(:project, namespace: group) }
+ let!(:project_member) { create(:project_member, :developer, user: user, project: project) }
+
+ it 'returns group web_url and avatar_url' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ group_data = json_response['namespace']
+ expect(group_data['web_url']).to eq(group.web_url)
+ expect(group_data['avatar_url']).to eq(group.avatar_url)
+ end
+ end
+
+ context 'when project belongs to a user namespace' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ it 'returns user web_url and avatar_url' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ user_data = json_response['namespace']
+ expect(user_data['web_url']).to eq("http://localhost/#{user.username}")
+ expect(user_data['avatar_url']).to eq(user.avatar_url)
+ end
+ end
+ end
+
+ context 'with external authorization' do
+ let(:project) do
+ create(:project,
+ namespace: user.namespace,
+ external_authorization_classification_label: 'the-label')
+ end
+
+ context 'when the user has access to the project' do
+ before do
+ external_service_allow_access(user, project)
+ end
+
+ it 'includes the label in the response' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['external_authorization_classification_label']).to eq('the-label')
+ end
+ end
+
+ context 'when the external service denies access' do
+ before do
+ external_service_deny_access(user, project)
+ end
+
+ it 'returns a 404' do
+ get api("/projects/#{project.id}", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
end
@@ -1385,7 +1571,7 @@ describe API::Projects do
end
it 'fails if forked_from project which does not exist' do
- post api("/projects/#{project_fork_target.id}/fork/9999", admin)
+ post api("/projects/#{project_fork_target.id}/fork/0", admin)
expect(response).to have_gitlab_http_status(404)
end
@@ -1510,6 +1696,9 @@ describe API::Projects do
describe "POST /projects/:id/share" do
let(:group) { create(:group) }
+ before do
+ group.add_developer(user)
+ end
it "shares project with group" do
expires_at = 10.days.from_now.to_date
@@ -1560,6 +1749,15 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(400)
expect(json_response['error']).to eq 'group_access does not have a valid value'
end
+
+ it "returns a 409 error when link is not saved" do
+ allow(::Projects::GroupLinks::CreateService).to receive_message_chain(:new, :execute)
+ .and_return({ status: :error, http_status: 409, message: 'error' })
+
+ post api("/projects/#{project.id}/share", user), params: { group_id: group.id, group_access: Gitlab::Access::DEVELOPER }
+
+ expect(response).to have_gitlab_http_status(409)
+ end
end
describe 'DELETE /projects/:id/share/:group_id' do
@@ -1807,6 +2005,20 @@ describe API::Projects do
expect(response).to have_gitlab_http_status(403)
end
end
+
+ context 'when updating external classification' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'updates the classification label' do
+ put(api("/projects/#{project.id}", user), params: { external_authorization_classification_label: 'new label' })
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(project.reload.external_authorization_classification_label).to eq('new label')
+ end
+ end
end
describe 'POST /projects/:id/archive' do
@@ -1936,7 +2148,7 @@ describe API::Projects do
end
it 'returns not_found(404) for not existing project' do
- get api("/projects/9999999999/languages", user)
+ get api("/projects/0/languages", user)
expect(response).to have_gitlab_http_status(:not_found)
end
diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb
index ba948e37e2f..3a59052bb29 100644
--- a/spec/requests/api/release/links_spec.rb
+++ b/spec/requests/api/release/links_spec.rb
@@ -73,6 +73,22 @@ describe API::Release::Links do
expect(response).to have_gitlab_http_status(:ok)
end
end
+
+ context 'when project is public and the repository is private' do
+ let(:project) { create(:project, :repository, :public, :repository_private) }
+
+ it_behaves_like '403 response' do
+ let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) }
+ end
+
+ context 'when the release does not exists' do
+ let!(:release) { }
+
+ it_behaves_like '403 response' do
+ let(:request) { get api("/projects/#{project.id}/releases/v0.1/assets/links", non_project_member) }
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/releases_spec.rb b/spec/requests/api/releases_spec.rb
index 1f317971a66..8603fa2a73d 100644
--- a/spec/requests/api/releases_spec.rb
+++ b/spec/requests/api/releases_spec.rb
@@ -4,12 +4,14 @@ describe API::Releases do
let(:project) { create(:project, :repository, :private) }
let(:maintainer) { create(:user) }
let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
let(:non_project_member) { create(:user) }
let(:commit) { create(:commit, project: project) }
before do
project.add_maintainer(maintainer)
project.add_reporter(reporter)
+ project.add_guest(guest)
project.repository.add_tag(maintainer, 'v0.1', commit.id)
project.repository.add_tag(maintainer, 'v0.2', commit.id)
@@ -50,7 +52,7 @@ describe API::Releases do
it 'matches response schema' do
get api("/projects/#{project.id}/releases", maintainer)
- expect(response).to match_response_schema('releases')
+ expect(response).to match_response_schema('public_api/v4/releases')
end
end
@@ -66,6 +68,46 @@ describe API::Releases do
end
end
+ context 'when user is a guest' do
+ let!(:release) do
+ create(:release,
+ project: project,
+ tag: 'v0.1',
+ author: maintainer,
+ created_at: 2.days.ago)
+ end
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/releases", guest)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it "does not expose tag, commit and source code" do
+ get api("/projects/#{project.id}/releases", guest)
+
+ expect(response).to match_response_schema('public_api/v4/release/releases_for_guest')
+ expect(json_response[0]['assets']['count']).to eq(release.links.count)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/releases", guest)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it "exposes tag, commit and source code" do
+ get api("/projects/#{project.id}/releases", guest)
+
+ expect(response).to match_response_schema('public_api/v4/releases')
+ expect(json_response[0]['assets']['count']).to eq(release.links.count + release.sources.count)
+ end
+ end
+ end
+
context 'when user is not a project member' do
it 'cannot find the project' do
get api("/projects/#{project.id}/releases", non_project_member)
@@ -115,7 +157,7 @@ describe API::Releases do
it 'matches response schema' do
get api("/projects/#{project.id}/releases/v0.1", maintainer)
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
it 'contains source information as assets' do
@@ -189,6 +231,35 @@ describe API::Releases do
end
end
end
+
+ context 'when user is a guest' do
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/releases/v0.1", guest)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :repository, :public) }
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/releases/v0.1", guest)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it "exposes tag and commit" do
+ create(:release,
+ project: project,
+ tag: 'v0.1',
+ author: maintainer,
+ created_at: 2.days.ago)
+ get api("/projects/#{project.id}/releases/v0.1", guest)
+
+ expect(response).to match_response_schema('public_api/v4/release')
+ end
+ end
+ end
end
context 'when specified tag is not found in the project' do
@@ -268,7 +339,7 @@ describe API::Releases do
it 'matches response schema' do
post api("/projects/#{project.id}/releases", maintainer), params: params
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
it 'does not create a new tag' do
@@ -340,7 +411,7 @@ describe API::Releases do
it 'matches response schema' do
post api("/projects/#{project.id}/releases", maintainer), params: params
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
end
@@ -494,7 +565,7 @@ describe API::Releases do
it 'matches response schema' do
put api("/projects/#{project.id}/releases/v0.1", maintainer), params: params
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
context 'when user tries to update sha' do
@@ -586,7 +657,7 @@ describe API::Releases do
it 'matches response schema' do
delete api("/projects/#{project.id}/releases/v0.1", maintainer)
- expect(response).to match_response_schema('release')
+ expect(response).to match_response_schema('public_api/v4/release')
end
context 'when there are no corresponding releases' do
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index e6c235ca26e..038c958b5cc 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -68,7 +68,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
post api('/runners'), params: { token: group.runners_token }
expect(response).to have_http_status 201
- expect(group.runners.size).to eq(1)
+ expect(group.runners.reload.size).to eq(1)
runner = Ci::Runner.first
expect(runner.token).not_to eq(registration_token)
expect(runner.token).not_to eq(group.runners_token)
@@ -168,6 +168,32 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'when access_level is provided for Runner' do
+ context 'when access_level is set to ref_protected' do
+ it 'creates runner' do
+ post api('/runners'), params: {
+ token: registration_token,
+ access_level: 'ref_protected'
+ }
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.ref_protected?).to be true
+ end
+ end
+
+ context 'when access_level is set to not_protected' do
+ it 'creates runner' do
+ post api('/runners'), params: {
+ token: registration_token,
+ access_level: 'not_protected'
+ }
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.ref_protected?).to be false
+ end
+ end
+ end
+
context 'when maximum job timeout is specified' do
it 'creates runner' do
post api('/runners'), params: {
@@ -418,8 +444,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
'sha' => job.sha,
'before_sha' => job.before_sha,
'ref_type' => 'branch',
- 'refspecs' => %w[+refs/heads/*:refs/remotes/origin/* +refs/tags/*:refs/tags/*],
- 'depth' => 0 }
+ 'refspecs' => ["+refs/heads/#{job.ref}:refs/remotes/origin/#{job.ref}"],
+ 'depth' => project.default_git_depth }
end
let(:expected_steps) do
@@ -436,9 +462,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
let(:expected_variables) do
- [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
- { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
- { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true }]
+ [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false },
+ { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false },
+ { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true, 'masked' => false }]
end
let(:expected_artifacts) do
@@ -470,11 +496,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(json_response['token']).to eq(job.token)
expect(json_response['job_info']).to eq(expected_job_info)
expect(json_response['git_info']).to eq(expected_git_info)
- expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh' })
+ expect(json_response['image']).to eq({ 'name' => 'ruby:2.1', 'entrypoint' => '/bin/sh', 'ports' => [] })
expect(json_response['services']).to eq([{ 'name' => 'postgres', 'entrypoint' => nil,
- 'alias' => nil, 'command' => nil },
+ 'alias' => nil, 'command' => nil, 'ports' => [] },
{ 'name' => 'docker:stable-dind', 'entrypoint' => '/bin/sh',
- 'alias' => 'docker', 'command' => 'sleep 30' }])
+ 'alias' => 'docker', 'command' => 'sleep 30', 'ports' => [] }])
expect(json_response['steps']).to eq(expected_steps)
expect(json_response['artifacts']).to eq(expected_artifacts)
expect(json_response['cache']).to eq(expected_cache)
@@ -505,7 +531,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
- context 'when GIT_DEPTH is not specified' do
+ context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do
+ before do
+ project.update!(default_git_depth: nil)
+ end
+
it 'specifies refspecs' do
request_job
@@ -516,6 +546,30 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'when job filtered by job_age' do
+ let!(:job) { create(:ci_build, :tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0, queued_at: 60.seconds.ago) }
+
+ context 'job is queued less than job_age parameter' do
+ let(:job_age) { 120 }
+
+ it 'gives 204' do
+ request_job(job_age: job_age)
+
+ expect(response).to have_gitlab_http_status(204)
+ end
+ end
+
+ context 'job is queued more than job_age parameter' do
+ let(:job_age) { 30 }
+
+ it 'picks a job' do
+ request_job(job_age: job_age)
+
+ expect(response).to have_gitlab_http_status(201)
+ end
+ end
+ end
+
context 'when job is made for branch' do
it 'sets tag as ref_type' do
request_job
@@ -537,7 +591,11 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
- context 'when GIT_DEPTH is not specified' do
+ context 'when GIT_DEPTH is not specified and there is no default git depth for the project' do
+ before do
+ project.update!(default_git_depth: nil)
+ end
+
it 'specifies refspecs' do
request_job
@@ -549,7 +607,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
context 'when job is made for merge request' do
- let(:pipeline) { create(:ci_pipeline_without_jobs, source: :merge_request, project: project, ref: 'feature', merge_request: merge_request) }
+ let(:pipeline) { create(:ci_pipeline_without_jobs, source: :merge_request_event, project: project, ref: 'feature', merge_request: merge_request) }
let!(:job) { create(:ci_build, pipeline: pipeline, name: 'spinach', ref: 'feature', stage: 'test', stage_idx: 0) }
let(:merge_request) { create(:merge_request) }
@@ -740,12 +798,12 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
context 'when triggered job is available' do
let(:expected_variables) do
- [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true },
- { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true },
- { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true },
- { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true },
- { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false },
- { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }]
+ [{ 'key' => 'CI_JOB_NAME', 'value' => 'spinach', 'public' => true, 'masked' => false },
+ { 'key' => 'CI_JOB_STAGE', 'value' => 'test', 'public' => true, 'masked' => false },
+ { 'key' => 'CI_PIPELINE_TRIGGERED', 'value' => 'true', 'public' => true, 'masked' => false },
+ { 'key' => 'DB_NAME', 'value' => 'postgres', 'public' => true, 'masked' => false },
+ { 'key' => 'SECRET_KEY', 'value' => 'secret_value', 'public' => false, 'masked' => false },
+ { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false, 'masked' => false }]
end
let(:trigger) { create(:ci_trigger, project: project) }
@@ -853,6 +911,56 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ describe 'port support' do
+ let(:job) { create(:ci_build, pipeline: pipeline, options: options) }
+
+ context 'when job image has ports' do
+ let(:options) do
+ {
+ image: {
+ name: 'ruby',
+ ports: [80]
+ },
+ services: ['mysql']
+ }
+ end
+
+ it 'returns the image ports' do
+ request_job
+
+ expect(response).to have_http_status(:created)
+ expect(json_response).to include(
+ 'id' => job.id,
+ 'image' => a_hash_including('name' => 'ruby', 'ports' => [{ 'number' => 80, 'protocol' => 'http', 'name' => 'default_port' }]),
+ 'services' => all(a_hash_including('name' => 'mysql')))
+ end
+ end
+
+ context 'when job services settings has ports' do
+ let(:options) do
+ {
+ image: 'ruby',
+ services: [
+ {
+ name: 'tomcat',
+ ports: [{ number: 8081, protocol: 'http', name: 'custom_port' }]
+ }
+ ]
+ }
+ end
+
+ it 'returns the service ports' do
+ request_job
+
+ expect(response).to have_http_status(:created)
+ expect(json_response).to include(
+ 'id' => job.id,
+ 'image' => a_hash_including('name' => 'ruby'),
+ 'services' => all(a_hash_including('name' => 'tomcat', 'ports' => [{ 'number' => 8081, 'protocol' => 'http', 'name' => 'custom_port' }])))
+ end
+ end
+ end
+
def request_job(token = runner.token, **params)
new_params = params.merge(token: token, last_update: last_update)
post api('/jobs/request'), params: new_params, headers: { 'User-Agent' => user_agent }
@@ -918,6 +1026,15 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it { expect(job).to be_job_execution_timeout }
end
+
+ context 'when failure_reason is unmet_prerequisites' do
+ before do
+ update_job(state: 'failed', failure_reason: 'unmet_prerequisites')
+ job.reload
+ end
+
+ it { expect(job).to be_unmet_prerequisites }
+ end
end
context 'when trace is given' do
@@ -1523,8 +1640,8 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
let!(:metadata) { file_upload2 }
let!(:metadata_sha256) { Digest::SHA256.file(metadata.path).hexdigest }
- let(:stored_artifacts_file) { job.reload.artifacts_file.file }
- let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
+ let(:stored_artifacts_file) { job.reload.artifacts_file }
+ let(:stored_metadata_file) { job.reload.artifacts_metadata }
let(:stored_artifacts_size) { job.reload.artifacts_size }
let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 }
let(:stored_metadata_sha256) { job.reload.job_artifacts_metadata.file_sha256 }
@@ -1545,9 +1662,9 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
it 'stores artifacts and artifacts metadata' do
expect(response).to have_gitlab_http_status(201)
- expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
- expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
- expect(stored_artifacts_size).to eq(72821)
+ expect(stored_artifacts_file.filename).to eq(artifacts.original_filename)
+ expect(stored_metadata_file.filename).to eq(metadata.original_filename)
+ expect(stored_artifacts_size).to eq(artifacts.size)
expect(stored_artifacts_sha256).to eq(artifacts_sha256)
expect(stored_metadata_sha256).to eq(metadata_sha256)
end
@@ -1675,7 +1792,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
it 'download artifacts' do
- expect(response).to have_http_status(200)
+ expect(response).to have_gitlab_http_status(200)
expect(response.headers.to_h).to include download_headers
end
end
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 7f11c8c9fe8..5548e3fd01a 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -90,6 +90,17 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(400)
end
+
+ it 'filters runners by tag_list' do
+ create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
+ create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2'])
+
+ get api('/runners?tag_list=tag1,tag2', user)
+
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Runner tagged with tag1 and tag2')
+ ]
+ end
end
context 'unauthorized user' do
@@ -181,6 +192,17 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(400)
end
+
+ it 'filters runners by tag_list' do
+ create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
+ create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2'])
+
+ get api('/runners/all?tag_list=tag1,tag2', admin)
+
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Runner tagged with tag1 and tag2')
+ ]
+ end
end
context 'without admin privileges' do
@@ -241,7 +263,7 @@ describe API::Runners do
end
it 'returns 404 if runner does not exists' do
- get api('/runners/9999', admin)
+ get api('/runners/0', admin)
expect(response).to have_gitlab_http_status(404)
end
@@ -394,7 +416,7 @@ describe API::Runners do
end
it 'returns 404 if runner does not exists' do
- update_runner(9999, admin, description: 'test')
+ update_runner(0, admin, description: 'test')
expect(response).to have_gitlab_http_status(404)
end
@@ -468,7 +490,7 @@ describe API::Runners do
end
it 'returns 404 if runner does not exists' do
- delete api('/runners/9999', admin)
+ delete api('/runners/0', admin)
expect(response).to have_gitlab_http_status(404)
end
@@ -573,7 +595,7 @@ describe API::Runners do
context "when runner doesn't exist" do
it 'returns 404' do
- get api('/runners/9999/jobs', admin)
+ get api('/runners/0/jobs', admin)
expect(response).to have_gitlab_http_status(404)
end
@@ -626,7 +648,7 @@ describe API::Runners do
context "when runner doesn't exist" do
it 'returns 404' do
- get api('/runners/9999/jobs', user)
+ get api('/runners/0/jobs', user)
expect(response).to have_gitlab_http_status(404)
end
@@ -716,6 +738,17 @@ describe API::Runners do
expect(response).to have_gitlab_http_status(400)
end
+
+ it 'filters runners by tag_list' do
+ create(:ci_runner, :project, description: 'Runner tagged with tag1 and tag2', projects: [project], tag_list: %w[tag1 tag2])
+ create(:ci_runner, :project, description: 'Runner tagged with tag2', projects: [project], tag_list: ['tag2'])
+
+ get api("/projects/#{project.id}/runners?tag_list=tag1,tag2", user)
+
+ expect(json_response).to match_array [
+ a_hash_including('description' => 'Runner tagged with tag1 and tag2')
+ ]
+ end
end
context 'authorized user without maintainer privileges' do
@@ -857,7 +890,7 @@ describe API::Runners do
end
it 'returns 404 is runner is not found' do
- delete api("/projects/#{project.id}/runners/9999", user)
+ delete api("/projects/#{project.id}/runners/0", user)
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/search_spec.rb b/spec/requests/api/search_spec.rb
index 831f47debeb..3e0b478abb3 100644
--- a/spec/requests/api/search_spec.rb
+++ b/spec/requests/api/search_spec.rb
@@ -70,11 +70,52 @@ describe API::Search do
context 'for milestones scope' do
before do
create(:milestone, project: project, title: 'awesome milestone')
+ end
+
+ context 'when user can read project milestones' do
+ before do
+ get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+ end
- get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+ it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
- it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+ context 'when user cannot read project milestones' do
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns empty array' do
+ get api('/search', user), params: { scope: 'milestones', search: 'awesome' }
+
+ milestones = JSON.parse(response.body)
+
+ expect(milestones).to be_empty
+ end
+ end
+ end
+
+ context 'for users scope' do
+ before do
+ create(:user, name: 'billy')
+
+ get api('/search', user), params: { scope: 'users', search: 'billy' }
+ end
+
+ it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
+
+ context 'when users search feature is disabled' do
+ before do
+ allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
+
+ get api('/search', user), params: { scope: 'users', search: 'billy' }
+ end
+
+ it 'returns 400 error' do
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
end
context 'for snippet_titles scope' do
@@ -126,7 +167,7 @@ describe API::Search do
context 'when group does not exist' do
it 'returns 404 error' do
- get api('/groups/9999/search', user), params: { scope: 'issues', search: 'awesome' }
+ get api('/groups/0/search', user), params: { scope: 'issues', search: 'awesome' }
expect(response).to have_gitlab_http_status(404)
end
@@ -192,6 +233,40 @@ describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
+
+ context 'for users scope' do
+ before do
+ user = create(:user, name: 'billy')
+ create(:group_member, :developer, user: user, group: group)
+
+ get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' }
+ end
+
+ it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
+
+ context 'when users search feature is disabled' do
+ before do
+ allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
+
+ get api("/groups/#{group.id}/search", user), params: { scope: 'users', search: 'billy' }
+ end
+
+ it 'returns 400 error' do
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ context 'for users scope with group path as id' do
+ before do
+ user1 = create(:user, name: 'billy')
+ create(:group_member, :developer, user: user1, group: group)
+
+ get api("/groups/#{CGI.escape(group.full_path)}/search", user), params: { scope: 'users', search: 'billy' }
+ end
+
+ it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
+ end
end
end
@@ -222,7 +297,7 @@ describe API::Search do
context 'when project does not exist' do
it 'returns 404 error' do
- get api('/projects/9999/search', user), params: { scope: 'issues', search: 'awesome' }
+ get api('/projects/0/search', user), params: { scope: 'issues', search: 'awesome' }
expect(response).to have_gitlab_http_status(404)
end
@@ -262,11 +337,53 @@ describe API::Search do
context 'for milestones scope' do
before do
create(:milestone, project: project, title: 'awesome milestone')
+ end
- get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ context 'when user can read milestones' do
+ before do
+ get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+ end
+
+ it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
end
- it_behaves_like 'response is correct', schema: 'public_api/v4/milestones'
+ context 'when user cannot read project milestones' do
+ before do
+ project.project_feature.update!(merge_requests_access_level: ProjectFeature::PRIVATE)
+ project.project_feature.update!(issues_access_level: ProjectFeature::PRIVATE)
+ end
+
+ it 'returns empty array' do
+ get api("/projects/#{project.id}/search", user), params: { scope: 'milestones', search: 'awesome' }
+
+ milestones = JSON.parse(response.body)
+
+ expect(milestones).to be_empty
+ end
+ end
+ end
+
+ context 'for users scope' do
+ before do
+ user1 = create(:user, name: 'billy')
+ create(:project_member, :developer, user: user1, project: project)
+
+ get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' }
+ end
+
+ it_behaves_like 'response is correct', schema: 'public_api/v4/user/basics'
+
+ context 'when users search feature is disabled' do
+ before do
+ allow(Feature).to receive(:disabled?).with(:users_search, default_enabled: true).and_return(true)
+
+ get api("/projects/#{project.id}/search", user), params: { scope: 'users', search: 'billy' }
+ end
+
+ it 'returns 400 error' do
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
end
context 'for notes scope' do
@@ -335,6 +452,13 @@ describe API::Search do
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to eq(11)
end
+
+ it 'by ref' do
+ get api("/projects/#{repo_project.id}/search", user), params: { scope: 'blobs', search: 'This file is used in tests for ci_environments_status', ref: 'pages-deploy' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(1)
+ end
end
end
end
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index f33eb5b9e02..8a60980fe80 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -13,6 +13,7 @@ describe API::Settings, 'Settings' do
expect(json_response['default_projects_limit']).to eq(42)
expect(json_response['password_authentication_enabled_for_web']).to be_truthy
expect(json_response['repository_storages']).to eq(['default'])
+ expect(json_response['password_authentication_enabled']).to be_truthy
expect(json_response['plantuml_enabled']).to be_falsey
expect(json_response['plantuml_url']).to be_nil
expect(json_response['default_project_visibility']).to be_a String
@@ -44,6 +45,7 @@ describe API::Settings, 'Settings' do
put api("/application/settings", admin),
params: {
default_projects_limit: 3,
+ default_project_creation: 2,
password_authentication_enabled_for_web: false,
repository_storages: ['custom'],
plantuml_enabled: true,
@@ -64,12 +66,13 @@ describe API::Settings, 'Settings' do
performance_bar_allowed_group_path: group.full_path,
instance_statistics_visibility_private: true,
diff_max_patch_bytes: 150_000,
- default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
+ default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3
}
expect(response).to have_gitlab_http_status(200)
expect(json_response['default_projects_limit']).to eq(3)
+ expect(json_response['default_project_creation']).to eq(::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS)
expect(json_response['password_authentication_enabled_for_web']).to be_falsey
expect(json_response['repository_storages']).to eq(['custom'])
expect(json_response['plantuml_enabled']).to be_truthy
@@ -114,6 +117,39 @@ describe API::Settings, 'Settings' do
expect(json_response['performance_bar_allowed_group_id']).to be_nil
end
+ context 'external policy classification settings' do
+ let(:settings) do
+ {
+ external_authorization_service_enabled: true,
+ external_authorization_service_url: 'https://custom.service/',
+ external_authorization_service_default_label: 'default',
+ external_authorization_service_timeout: 9.99,
+ external_auth_client_cert: File.read('spec/fixtures/passphrase_x509_certificate.crt'),
+ external_auth_client_key: File.read('spec/fixtures/passphrase_x509_certificate_pk.key'),
+ external_auth_client_key_pass: "5iveL!fe"
+ }
+ 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(200)
+ 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(200)
+ settings.each do |attribute, value|
+ expect(ApplicationSetting.current.public_send(attribute)).to eq(value)
+ end
+ end
+ end
+
context "missing plantuml_url value when plantuml_enabled is true" do
it "returns a blank parameter error message" do
put api("/application/settings", admin), params: { plantuml_enabled: true }
diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb
index 7c8512f7589..d600076e9fb 100644
--- a/spec/requests/api/snippets_spec.rb
+++ b/spec/requests/api/snippets_spec.rb
@@ -84,10 +84,17 @@ describe API::Snippets do
end
describe 'GET /snippets/:id/raw' do
- let(:snippet) { create(:personal_snippet, author: user) }
+ set(:author) { create(:user) }
+ set(:snippet) { create(:personal_snippet, :private, author: author) }
+
+ it 'requires authentication' do
+ get api("/snippets/#{snippet.id}", nil)
+
+ expect(response).to have_gitlab_http_status(401)
+ end
it 'returns raw text' do
- get api("/snippets/#{snippet.id}/raw", user)
+ get api("/snippets/#{snippet.id}/raw", author)
expect(response).to have_gitlab_http_status(200)
expect(response.content_type).to eq 'text/plain'
@@ -95,38 +102,83 @@ describe API::Snippets do
end
it 'forces attachment content disposition' do
- get api("/snippets/#{snippet.id}/raw", user)
+ get api("/snippets/#{snippet.id}/raw", author)
expect(headers['Content-Disposition']).to match(/^attachment/)
end
it 'returns 404 for invalid snippet id' do
- get api("/snippets/1234/raw", user)
+ snippet.destroy
+
+ get api("/snippets/#{snippet.id}/raw", author)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 Snippet Not Found')
end
+
+ it 'hides private snippets from ordinary users' do
+ get api("/snippets/#{snippet.id}/raw", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'shows internal snippets to ordinary users' do
+ internal_snippet = create(:personal_snippet, :internal, author: author)
+
+ get api("/snippets/#{internal_snippet.id}/raw", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
end
describe 'GET /snippets/:id' do
- let(:snippet) { create(:personal_snippet, author: user) }
+ set(:admin) { create(:user, :admin) }
+ set(:author) { create(:user) }
+ set(:private_snippet) { create(:personal_snippet, :private, author: author) }
+ set(:internal_snippet) { create(:personal_snippet, :internal, author: author) }
+
+ it 'requires authentication' do
+ get api("/snippets/#{private_snippet.id}", nil)
+
+ expect(response).to have_gitlab_http_status(401)
+ end
it 'returns snippet json' do
- get api("/snippets/#{snippet.id}", user)
+ get api("/snippets/#{private_snippet.id}", author)
expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(snippet.title)
- expect(json_response['description']).to eq(snippet.description)
- expect(json_response['file_name']).to eq(snippet.file_name)
- expect(json_response['visibility']).to eq(snippet.visibility)
+ expect(json_response['title']).to eq(private_snippet.title)
+ expect(json_response['description']).to eq(private_snippet.description)
+ expect(json_response['file_name']).to eq(private_snippet.file_name)
+ expect(json_response['visibility']).to eq(private_snippet.visibility)
+ end
+
+ it 'shows private snippets to an admin' do
+ get api("/snippets/#{private_snippet.id}", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'hides private snippets from an ordinary user' do
+ get api("/snippets/#{private_snippet.id}", user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+
+ it 'shows internal snippets to an ordinary user' do
+ get api("/snippets/#{internal_snippet.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
end
it 'returns 404 for invalid snippet id' do
- get api("/snippets/1234", user)
+ private_snippet.destroy
+
+ get api("/snippets/#{private_snippet.id}", admin)
expect(response).to have_gitlab_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['message']).to eq('404 Snippet Not Found')
end
end
diff --git a/spec/requests/api/suggestions_spec.rb b/spec/requests/api/suggestions_spec.rb
index 3c2842e5725..5b07e598b8d 100644
--- a/spec/requests/api/suggestions_spec.rb
+++ b/spec/requests/api/suggestions_spec.rb
@@ -42,8 +42,7 @@ describe API::Suggestions do
expect(response).to have_gitlab_http_status(200)
expect(json_response)
- .to include('id', 'from_original_line', 'to_original_line',
- 'from_line', 'to_line', 'appliable', 'applied',
+ .to include('id', 'from_line', 'to_line', 'appliable', 'applied',
'from_content', 'to_content')
end
end
diff --git a/spec/requests/api/system_hooks_spec.rb b/spec/requests/api/system_hooks_spec.rb
index b6e8d74c2e9..0e2f3face71 100644
--- a/spec/requests/api/system_hooks_spec.rb
+++ b/spec/requests/api/system_hooks_spec.rb
@@ -1,12 +1,14 @@
require 'spec_helper'
describe API::SystemHooks do
+ include StubRequests
+
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let!(:hook) { create(:system_hook, url: "http://example.com") }
before do
- stub_request(:post, hook.url)
+ stub_full_request(hook.url, method: :post)
end
describe "GET /hooks" do
@@ -68,6 +70,8 @@ describe API::SystemHooks do
end
it 'sets default values for events' do
+ stub_full_request('http://mep.mep', method: :post)
+
post api('/hooks', admin), params: { url: 'http://mep.mep' }
expect(response).to have_gitlab_http_status(201)
@@ -78,6 +82,8 @@ describe API::SystemHooks do
end
it 'sets explicit values for events' do
+ stub_full_request('http://mep.mep', method: :post)
+
post api('/hooks', admin),
params: {
url: 'http://mep.mep',
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index fffe878ddbd..d898319e709 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -378,7 +378,7 @@ describe API::Tags do
post api(route, user), params: { description: description }
expect(response).to have_gitlab_http_status(201)
- expect(response).to match_response_schema('public_api/v4/release')
+ expect(response).to match_response_schema('public_api/v4/release/tag_release')
expect(json_response['tag_name']).to eq(tag_name)
expect(json_response['description']).to eq(description)
end
diff --git a/spec/requests/api/task_completion_status_spec.rb b/spec/requests/api/task_completion_status_spec.rb
new file mode 100644
index 00000000000..ee2531197b1
--- /dev/null
+++ b/spec/requests/api/task_completion_status_spec.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'task completion status response' do
+ set(:user) { create(:user) }
+ set(:project) do
+ create(:project, :public, creator_id: user.id, namespace: user.namespace)
+ end
+
+ shared_examples 'taskable completion status provider' do |path|
+ samples = [
+ {
+ description: '',
+ expected_count: 0,
+ expected_completed_count: 0
+ },
+ {
+ description: 'Lorem ipsum',
+ expected_count: 0,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [ ] task 1
+ - [x] task 2 },
+ expected_count: 2,
+ expected_completed_count: 1
+ },
+ {
+ description: %{- [ ] task 1
+ - [ ] task 2 },
+ expected_count: 2,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [x] task 1
+ - [x] task 2 },
+ expected_count: 2,
+ expected_completed_count: 2
+ },
+ {
+ description: %{- [ ] task 1},
+ expected_count: 1,
+ expected_completed_count: 0
+ },
+ {
+ description: %{- [x] task 1},
+ expected_count: 1,
+ expected_completed_count: 1
+ }
+ ]
+ samples.each do |sample_data|
+ context "with a description of #{sample_data[:description].inspect}" do
+ before do
+ taskable.update!(description: sample_data[:description])
+
+ get api("#{path}?iids[]=#{taskable.iid}", user)
+ end
+
+ it { expect(response).to have_gitlab_http_status(200) }
+
+ it 'returns the expected results' do
+ expect(json_response).to be_an Array
+ expect(json_response).not_to be_empty
+
+ task_completion_status = json_response.first['task_completion_status']
+ expect(task_completion_status['count']).to eq(sample_data[:expected_count])
+ expect(task_completion_status['completed_count']).to eq(sample_data[:expected_completed_count])
+ end
+ end
+ end
+ end
+
+ context 'task list completion status for issues' do
+ it_behaves_like 'taskable completion status provider', '/issues' do
+ let(:taskable) { create(:issue, project: project, author: user) }
+ end
+ end
+
+ context 'task list completion status for merge_requests' do
+ it_behaves_like 'taskable completion status provider', '/merge_requests' do
+ let(:taskable) { create(:merge_request, source_project: project, target_project: project, author: user) }
+ end
+ end
+end
diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb
index f121a1d3b78..9f0d5ad5d12 100644
--- a/spec/requests/api/todos_spec.rb
+++ b/spec/requests/api/todos_spec.rb
@@ -8,10 +8,14 @@ describe API::Todos do
let(:author_2) { create(:user) }
let(:john_doe) { create(:user, username: 'john_doe') }
let(:merge_request) { create(:merge_request, source_project: project_1) }
+ let!(:merge_request_todo) { create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request) }
let!(:pending_1) { create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe) }
let!(:pending_2) { create(:todo, project: project_2, author: author_2, user: john_doe) }
let!(:pending_3) { create(:on_commit_todo, project: project_1, author: author_2, user: john_doe) }
let!(:done) { create(:todo, :done, project: project_1, author: author_1, user: john_doe) }
+ let!(:award_emoji_1) { create(:award_emoji, awardable: merge_request, user: author_1, name: 'thumbsup') }
+ let!(:award_emoji_2) { create(:award_emoji, awardable: pending_1.target, user: author_1, name: 'thumbsup') }
+ let!(:award_emoji_3) { create(:award_emoji, awardable: pending_2.target, user: author_2, name: 'thumbsdown') }
before do
project_1.add_developer(john_doe)
@@ -34,7 +38,7 @@ describe API::Todos do
expect(response.status).to eq(200)
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)
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
@@ -45,6 +49,23 @@ describe API::Todos do
expect(json_response[0]['state']).to eq('pending')
expect(json_response[0]['action_name']).to eq('assigned')
expect(json_response[0]['created_at']).to be_present
+ expect(json_response[0]['target_type']).to eq('Commit')
+
+ expect(json_response[1]['target_type']).to eq('Issue')
+ expect(json_response[1]['target']['upvotes']).to eq(0)
+ expect(json_response[1]['target']['downvotes']).to eq(1)
+ expect(json_response[1]['target']['merge_requests_count']).to eq(0)
+
+ expect(json_response[2]['target_type']).to eq('Issue')
+ expect(json_response[2]['target']['upvotes']).to eq(1)
+ expect(json_response[2]['target']['downvotes']).to eq(0)
+ expect(json_response[2]['target']['merge_requests_count']).to eq(0)
+
+ expect(json_response[3]['target_type']).to eq('MergeRequest')
+ # Only issues get a merge request count at the moment
+ 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)
end
context 'and using the author filter' do
@@ -54,7 +75,7 @@ describe API::Todos do
expect(response.status).to eq(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect(json_response.length).to eq(3)
end
end
@@ -67,7 +88,7 @@ describe API::Todos do
expect(response.status).to eq(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect(json_response.length).to eq(2)
end
end
@@ -100,7 +121,7 @@ describe API::Todos do
expect(response.status).to eq(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(2)
+ expect(json_response.length).to eq(3)
end
end
@@ -115,6 +136,27 @@ describe API::Todos do
end
end
end
+
+ it 'avoids N+1 queries', :request_store do
+ create(:todo, project: project_1, author: author_2, user: john_doe, target: merge_request)
+
+ get api('/todos', john_doe)
+
+ control = ActiveRecord::QueryRecorder.new { get api('/todos', john_doe) }
+
+ merge_request_2 = create(:merge_request, source_project: project_2)
+ create(:todo, project: project_2, author: author_2, user: john_doe, target: merge_request_2)
+
+ project_3 = create(:project, :repository)
+ project_3.add_developer(john_doe)
+ merge_request_3 = create(:merge_request, source_project: project_3)
+ create(:todo, project: project_3, author: author_2, user: john_doe, target: merge_request_3)
+ create(:todo, :mentioned, project: project_1, author: author_1, user: john_doe)
+ create(:on_commit_todo, project: project_3, author: author_1, user: john_doe)
+
+ expect { get api('/todos', john_doe) }.not_to exceed_query_limit(control)
+ expect(response.status).to eq(200)
+ end
end
describe 'POST /todos/:id/mark_as_done' do
@@ -230,7 +272,7 @@ describe API::Todos do
context 'for a merge request' do
it_behaves_like 'an issuable', 'merge_requests' do
- let(:issuable) { merge_request }
+ let(:issuable) { create(:merge_request, :simple, source_project: project_1) }
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index b381431306d..bab1520b960 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -68,6 +68,13 @@ describe API::Users do
expect(json_response.size).to eq(0)
end
+ it "does not return the highest role" do
+ get api("/users"), params: { username: user.username }
+
+ expect(response).to match_response_schema('public_api/v4/user/basics')
+ expect(json_response.first.keys).not_to include 'highest_role'
+ end
+
context "when public level is restricted" do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
@@ -269,6 +276,18 @@ describe API::Users do
expect(response).to have_gitlab_http_status(400)
end
end
+
+ context "when authenticated and ldap is enabled" do
+ it "returns non-ldap user" do
+ create :omniauth_user, provider: "ldapserver1"
+
+ get api("/users", user), params: { skip_ldap: "true" }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first["username"]).to eq user.username
+ end
+ end
end
describe "GET /users/:id" do
@@ -286,6 +305,13 @@ describe API::Users do
expect(json_response.keys).not_to include 'is_admin'
end
+ it "does not return the user's `highest_role`" do
+ get api("/users/#{user.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/user/basic')
+ expect(json_response.keys).not_to include 'highest_role'
+ end
+
context 'when authenticated as admin' do
it 'includes the `is_admin` field' do
get api("/users/#{user.id}", admin)
@@ -300,6 +326,12 @@ describe API::Users do
expect(response).to match_response_schema('public_api/v4/user/admin')
expect(json_response.keys).to include 'created_at'
end
+ it 'includes the `highest_role` field' do
+ get api("/users/#{user.id}", admin)
+
+ expect(response).to match_response_schema('public_api/v4/user/admin')
+ expect(json_response['highest_role']).to be(0)
+ end
end
context 'for an anonymous user' do
@@ -335,7 +367,7 @@ describe API::Users do
end
it "returns a 404 error if user id not found" do
- get api("/users/9999", user)
+ get api("/users/0", user)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -732,7 +764,7 @@ describe API::Users do
end
it "returns 404 for non-existing user" do
- put api("/users/999999", admin), params: { bio: 'update should fail' }
+ put api("/users/0", admin), params: { bio: 'update should fail' }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -836,7 +868,7 @@ describe API::Users do
end
it "returns 400 for invalid ID" do
- post api("/users/999999/keys", admin)
+ post api("/users/0/keys", admin)
expect(response).to have_gitlab_http_status(400)
end
end
@@ -895,7 +927,7 @@ describe API::Users do
it 'returns 404 error if user not found' do
user.keys << key
user.save
- delete api("/users/999999/keys/#{key.id}", admin)
+ delete api("/users/0/keys/#{key.id}", admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -930,7 +962,7 @@ describe API::Users do
end
it 'returns 400 for invalid ID' do
- post api('/users/999999/gpg_keys', admin)
+ post api('/users/0/gpg_keys', admin)
expect(response).to have_gitlab_http_status(400)
end
@@ -951,7 +983,7 @@ describe API::Users do
context 'when authenticated' do
it 'returns 404 for non-existing user' do
- get api('/users/999999/gpg_keys', admin)
+ get api('/users/0/gpg_keys', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -1007,7 +1039,7 @@ describe API::Users do
user.keys << key
user.save
- delete api("/users/999999/gpg_keys/#{gpg_key.id}", admin)
+ delete api("/users/0/gpg_keys/#{gpg_key.id}", admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -1051,7 +1083,7 @@ describe API::Users do
user.gpg_keys << gpg_key
user.save
- post api("/users/999999/gpg_keys/#{gpg_key.id}/revoke", admin)
+ post api("/users/0/gpg_keys/#{gpg_key.id}/revoke", admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
@@ -1089,7 +1121,7 @@ describe API::Users do
end
it "returns a 400 for invalid ID" do
- post api("/users/999999/emails", admin)
+ post api("/users/0/emails", admin)
expect(response).to have_gitlab_http_status(400)
end
@@ -1121,7 +1153,7 @@ describe API::Users do
context 'when authenticated' do
it 'returns 404 for non-existing user' do
- get api('/users/999999/emails', admin)
+ get api('/users/0/emails', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1177,7 +1209,7 @@ describe API::Users do
it 'returns 404 error if user not found' do
user.emails << email
user.save
- delete api("/users/999999/emails/#{email.id}", admin)
+ delete api("/users/0/emails/#{email.id}", admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1227,7 +1259,7 @@ describe API::Users do
end
it "returns 404 for non-existing user" do
- perform_enqueued_jobs { delete api("/users/999999", admin) }
+ perform_enqueued_jobs { delete api("/users/0", admin) }
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1778,7 +1810,7 @@ describe API::Users do
end
it 'returns a 404 error if user id not found' do
- post api('/users/9999/block', admin)
+ post api('/users/0/block', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
@@ -1816,7 +1848,7 @@ describe API::Users do
end
it 'returns a 404 error if user id not found' do
- post api('/users/9999/block', admin)
+ post api('/users/0/block', admin)
expect(response).to have_gitlab_http_status(404)
expect(json_response['message']).to eq('404 User Not Found')
end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index cdac5b2f400..55b1419a004 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -43,6 +43,8 @@ describe API::Variables do
expect(response).to have_gitlab_http_status(200)
expect(json_response['value']).to eq(variable.value)
expect(json_response['protected']).to eq(variable.protected?)
+ expect(json_response['masked']).to eq(variable.masked?)
+ expect(json_response['variable_type']).to eq('env_var')
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -73,24 +75,28 @@ describe API::Variables do
context 'authorized user with proper permissions' do
it 'creates variable' do
expect do
- post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true }
+ post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'PROTECTED_VALUE_2', protected: true, masked: true }
end.to change {project.variables.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
- expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['value']).to eq('PROTECTED_VALUE_2')
expect(json_response['protected']).to be_truthy
+ expect(json_response['masked']).to be_truthy
+ expect(json_response['variable_type']).to eq('env_var')
end
it 'creates variable with optional attributes' do
expect do
- post api("/projects/#{project.id}/variables", user), params: { key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
+ post api("/projects/#{project.id}/variables", user), params: { variable_type: 'file', key: 'TEST_VARIABLE_2', value: 'VALUE_2' }
end.to change {project.variables.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
expect(json_response['protected']).to be_falsey
+ expect(json_response['masked']).to be_falsey
+ expect(json_response['variable_type']).to eq('file')
end
it 'does not allow to duplicate variable key' do
@@ -125,7 +131,7 @@ describe API::Variables do
initial_variable = project.variables.reload.first
value_before = initial_variable.value
- put api("/projects/#{project.id}/variables/#{variable.key}", user), params: { value: 'VALUE_1_UP', protected: true }
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), params: { variable_type: 'file', value: 'VALUE_1_UP', protected: true }
updated_variable = project.variables.reload.first
@@ -133,6 +139,7 @@ describe API::Variables do
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
expect(updated_variable).to be_protected
+ expect(updated_variable.variable_type).to eq('file')
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/requests/api/version_spec.rb b/spec/requests/api/version_spec.rb
index 38b618191fb..e06f8bbc095 100644
--- a/spec/requests/api/version_spec.rb
+++ b/spec/requests/api/version_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe API::Version do
- describe 'GET /version' do
+ shared_examples_for 'GET /version' do
context 'when unauthenticated' do
it 'returns authentication error' do
get api('/version')
@@ -22,4 +22,20 @@ describe API::Version do
end
end
end
+
+ context 'with graphql enabled' do
+ before do
+ stub_feature_flags(graphql: true)
+ end
+
+ include_examples 'GET /version'
+ end
+
+ context 'with graphql disabled' do
+ before do
+ stub_feature_flags(graphql: false)
+ end
+
+ include_examples 'GET /version'
+ end
end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 5b625fd47be..5c9a5b73ee5 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -104,6 +104,70 @@ describe 'Git HTTP requests' do
end
end
+ shared_examples_for 'project path without .git suffix' do
+ context "GET info/refs" do
+ let(:path) { "/#{project_path}/info/refs" }
+
+ context "when no params are added" do
+ before do
+ get path
+ end
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project_path}.git/info/refs")
+ end
+ end
+
+ context "when the upload-pack service is requested" do
+ let(:params) { { service: 'git-upload-pack' } }
+
+ before do
+ get path, params: params
+ end
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}")
+ end
+ end
+
+ context "when the receive-pack service is requested" do
+ let(:params) { { service: 'git-receive-pack' } }
+
+ before do
+ get path, params: params
+ end
+
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}")
+ end
+ end
+
+ context "when the params are anything else" do
+ let(:params) { { service: 'git-implode-pack' } }
+
+ before do
+ get path, params: params
+ end
+
+ it "redirects to the sign-in page" do
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "POST git-upload-pack" do
+ it "fails to find a route" do
+ expect { clone_post(project_path) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+
+ context "POST git-receive-pack" do
+ it "fails to find a route" do
+ expect { push_post(project_path) }.to raise_error(ActionController::RoutingError)
+ end
+ end
+ end
+
describe "User with no identities" do
let(:user) { create(:user) }
@@ -143,6 +207,10 @@ describe 'Git HTTP requests' do
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
+
+ it_behaves_like 'project path without .git suffix' do
+ let(:project_path) { "#{user.namespace.path}/project.git-project" }
+ end
end
end
@@ -481,14 +549,14 @@ describe 'Git HTTP requests' do
it 'rejects pulls with personal access token error message' do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
it 'rejects the push attempt with personal access token error message' do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
end
@@ -498,6 +566,47 @@ describe 'Git HTTP requests' do
it_behaves_like 'pulls are allowed'
it_behaves_like 'pushes are allowed'
+
+ it 'rejects the push attempt for read_repository scope' do
+ read_access_token = create(:personal_access_token, user: user, scopes: [:read_repository])
+
+ upload(path, user: user.username, password: read_access_token.token) do |response|
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response.body).to include('You are not allowed to upload code')
+ end
+ end
+
+ it 'accepts the push attempt for write_repository scope' do
+ write_access_token = create(:personal_access_token, user: user, scopes: [:write_repository])
+
+ upload(path, user: user.username, password: write_access_token.token) do |response|
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it 'accepts the pull attempt for read_repository scope' do
+ read_access_token = create(:personal_access_token, user: user, scopes: [:read_repository])
+
+ download(path, user: user.username, password: read_access_token.token) do |response|
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it 'accepts the pull attempt for api scope' do
+ read_access_token = create(:personal_access_token, user: user, scopes: [:api])
+
+ download(path, user: user.username, password: read_access_token.token) do |response|
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ it 'accepts the push attempt for api scope' do
+ write_access_token = create(:personal_access_token, user: user, scopes: [:api])
+
+ upload(path, user: user.username, password: write_access_token.token) do |response|
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
end
end
@@ -509,14 +618,14 @@ describe 'Git HTTP requests' do
it 'rejects pulls with personal access token error message' do
download(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
it 'rejects pushes with personal access token error message' do
upload(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ expect(response.body).to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
@@ -530,7 +639,7 @@ describe 'Git HTTP requests' do
it 'does not display the personal access token error message' do
upload(path, user: 'foo', password: 'bar') do |response|
expect(response).to have_gitlab_http_status(:unauthorized)
- expect(response.body).not_to include('You must use a personal access token with \'api\' scope for Git over HTTP')
+ expect(response.body).not_to include('You must use a personal access token with \'read_repository\' or \'write_repository\' scope for Git over HTTP')
end
end
end
@@ -706,70 +815,8 @@ describe 'Git HTTP requests' do
end
end
- context "when the project path doesn't end in .git" do
- let(:project) { create(:project, :repository, :public, path: 'project.git-project') }
-
- context "GET info/refs" do
- let(:path) { "/#{project.full_path}/info/refs" }
-
- context "when no params are added" do
- before do
- get path
- end
-
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.full_path}.git/info/refs")
- end
- end
-
- context "when the upload-pack service is requested" do
- let(:params) { { service: 'git-upload-pack' } }
-
- before do
- get path, params: params
- end
-
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}")
- end
- end
-
- context "when the receive-pack service is requested" do
- let(:params) { { service: 'git-receive-pack' } }
-
- before do
- get path, params: params
- end
-
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.full_path}.git/info/refs?service=#{params[:service]}")
- end
- end
-
- context "when the params are anything else" do
- let(:params) { { service: 'git-implode-pack' } }
-
- before do
- get path, params: params
- end
-
- it "redirects to the sign-in page" do
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
-
- context "POST git-upload-pack" do
- it "fails to find a route" do
- expect { clone_post(project.full_path) }.to raise_error(ActionController::RoutingError)
- end
- end
-
- context "POST git-receive-pack" do
- it "fails to find a route" do
- expect { push_post(project.full_path) }.to raise_error(ActionController::RoutingError)
- end
- end
+ it_behaves_like 'project path without .git suffix' do
+ let(:project_path) { create(:project, :repository, :public, path: 'project.git-project').full_path }
end
context "retrieving an info/refs file" do
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 4bb3b848e17..bba473f1c20 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -142,7 +142,7 @@ describe JwtController do
end
it 'allows read access' do
- expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_authentication_abilities)
+ expect(service).to receive(:execute).with(authentication_abilities: Gitlab::Auth.read_only_authentication_abilities)
get '/jwt/auth', params: parameters
end
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index 2a455523e2c..86e41cbdf00 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -187,7 +187,7 @@ describe 'OpenID Connect requests' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['issuer']).to eq('http://localhost')
expect(json_response['jwks_uri']).to eq('http://www.example.com/oauth/discovery/keys')
- expect(json_response['scopes_supported']).to eq(%w[api read_user sudo read_repository openid profile email])
+ expect(json_response['scopes_supported']).to eq(%w[api read_user read_repository write_repository sudo openid profile email])
end
end
diff --git a/spec/requests/rack_attack_global_spec.rb b/spec/requests/rack_attack_global_spec.rb
index 49021f5d1b7..89adbc77a7f 100644
--- a/spec/requests/rack_attack_global_spec.rb
+++ b/spec/requests/rack_attack_global_spec.rb
@@ -182,6 +182,17 @@ describe 'Rack Attack global throttles' do
end
end
end
+
+ it 'logs RackAttack info into structured logs' do
+ requests_per_period.times do
+ get url_that_does_not_require_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect(Gitlab::AuthLogger).to receive(:error).once
+
+ get url_that_does_not_require_authentication
+ end
end
context 'when the throttle is disabled' do
@@ -251,8 +262,8 @@ describe 'Rack Attack global throttles' do
let(:throttle_setting_prefix) { 'throttle_authenticated_web' }
context 'with the token in the query string' do
- let(:get_args) { [rss_url(user), nil] }
- let(:other_user_get_args) { [rss_url(other_user), nil] }
+ let(:get_args) { [rss_url(user), params: nil] }
+ let(:other_user_get_args) { [rss_url(other_user), params: nil] }
it_behaves_like 'rate-limited token-authenticated requests'
end
@@ -327,6 +338,17 @@ describe 'Rack Attack global throttles' do
expect_rejection { get url_that_requires_authentication }
end
+
+ it 'logs RackAttack info into structured logs' do
+ requests_per_period.times do
+ get url_that_requires_authentication
+ expect(response).to have_http_status 200
+ end
+
+ expect(Gitlab::AuthLogger).to receive(:error).once
+
+ get url_that_requires_authentication
+ end
end
context 'when the throttle is disabled' do
diff --git a/spec/routing/api_routing_spec.rb b/spec/routing/api_routing_spec.rb
index 5fde4bd885b..3c48ead4ff2 100644
--- a/spec/routing/api_routing_spec.rb
+++ b/spec/routing/api_routing_spec.rb
@@ -7,25 +7,17 @@ describe 'api', 'routing' do
end
it 'does not route to the GraphqlController' do
- expect(get('/api/graphql')).not_to route_to('graphql#execute')
- end
-
- it 'does not expose graphiql' do
- expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show')
+ expect(post('/api/graphql')).not_to route_to('graphql#execute')
end
end
- context 'when graphql is disabled' do
+ context 'when graphql is enabled' do
before do
stub_feature_flags(graphql: true)
end
it 'routes to the GraphqlController' do
- expect(get('/api/graphql')).not_to route_to('graphql#execute')
- end
-
- it 'exposes graphiql' do
- expect(get('/-/graphql-explorer')).not_to route_to('graphiql/rails/editors#show')
+ expect(post('/api/graphql')).to route_to('graphql#execute')
end
end
end
diff --git a/spec/routing/group_routing_spec.rb b/spec/routing/group_routing_spec.rb
index 71788028cbf..00ca394a50b 100644
--- a/spec/routing/group_routing_spec.rb
+++ b/spec/routing/group_routing_spec.rb
@@ -17,6 +17,10 @@ describe "Groups", "routing" do
expect(get("/#{group_path}")).to route_to('groups#show', id: group_path)
end
+ it "to #details" do
+ expect(get("/groups/#{group_path}/-/details")).to route_to('groups#details', id: group_path)
+ end
+
it "to #activity" do
expect(get("/groups/#{group_path}/-/activity")).to route_to('groups#activity', id: group_path)
end
@@ -129,5 +133,19 @@ describe "Groups", "routing" do
let(:resource) { create(:group, parent: parent, path: 'activity') }
end
end
+
+ describe 'subgroup "boards"' do
+ it 'shows group show page' do
+ allow(Group).to receive(:find_by_full_path).with('gitlabhq/boards', any_args).and_return(true)
+
+ expect(get('/groups/gitlabhq/boards')).to route_to('groups#show', id: 'gitlabhq/boards')
+ end
+
+ it 'shows boards index page' do
+ allow(Group).to receive(:find_by_full_path).with('gitlabhq', any_args).and_return(true)
+
+ expect(get('/groups/gitlabhq/-/boards')).to route_to('groups/boards#index', group_id: 'gitlabhq')
+ end
+ end
end
end
diff --git a/spec/routing/import_routing_spec.rb b/spec/routing/import_routing_spec.rb
index 106f92082e4..3fdede7914d 100644
--- a/spec/routing/import_routing_spec.rb
+++ b/spec/routing/import_routing_spec.rb
@@ -174,3 +174,15 @@ describe Import::GitlabProjectsController, 'routing' do
expect(get('/import/gitlab_project/new')).to route_to('import/gitlab_projects#new')
end
end
+
+# new_import_phabricator GET /import/phabricator/new(.:format) import/phabricator#new
+# import_phabricator POST /import/phabricator(.:format) import/phabricator#create
+describe Import::PhabricatorController, 'routing' do
+ it 'to #create' do
+ expect(post("/import/phabricator")).to route_to("import/phabricator#create")
+ end
+
+ it 'to #new' do
+ expect(get("/import/phabricator/new")).to route_to("import/phabricator#new")
+ end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index a0d01fc8263..83775b1040e 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -138,9 +138,11 @@ describe 'project routing' do
describe Projects::AutocompleteSourcesController, 'routing' do
[:members, :issues, :merge_requests, :labels, :milestones, :commands, :snippets].each do |action|
it "to ##{action}" do
- expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get("/gitlab/gitlabhq/-/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
end
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/autocomplete_sources/labels", "/gitlab/gitlabhq/-/autocomplete_sources/labels"
end
# pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages
@@ -204,25 +206,27 @@ describe 'project routing' do
describe Projects::BranchesController, 'routing' do
it 'to #branches' do
- expect(get('/gitlab/gitlabhq/branches')).to route_to('projects/branches#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
- expect(delete('/gitlab/gitlabhq/branches/feature%2345')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
- expect(delete('/gitlab/gitlabhq/branches/feature%2B45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
- expect(delete('/gitlab/gitlabhq/branches/feature@45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
- expect(delete('/gitlab/gitlabhq/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz')
- expect(delete('/gitlab/gitlabhq/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
- expect(delete('/gitlab/gitlabhq/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
+ expect(get('/gitlab/gitlabhq/-/branches')).to route_to('projects/branches#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(delete('/gitlab/gitlabhq/-/branches/feature%2345')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
+ expect(delete('/gitlab/gitlabhq/-/branches/feature%2B45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
+ expect(delete('/gitlab/gitlabhq/-/branches/feature@45')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
+ expect(delete('/gitlab/gitlabhq/-/branches/feature%2345/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz')
+ expect(delete('/gitlab/gitlabhq/-/branches/feature%2B45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
+ expect(delete('/gitlab/gitlabhq/-/branches/feature@45/foo/bar/baz')).to route_to('projects/branches#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/branches", "/gitlab/gitlabhq/-/branches"
end
describe Projects::TagsController, 'routing' do
it 'to #tags' do
- expect(get('/gitlab/gitlabhq/tags')).to route_to('projects/tags#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
- expect(delete('/gitlab/gitlabhq/tags/feature%2345')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
- expect(delete('/gitlab/gitlabhq/tags/feature%2B45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
- expect(delete('/gitlab/gitlabhq/tags/feature@45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
- expect(delete('/gitlab/gitlabhq/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz')
- expect(delete('/gitlab/gitlabhq/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
- expect(delete('/gitlab/gitlabhq/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
+ expect(get('/gitlab/gitlabhq/-/tags')).to route_to('projects/tags#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(delete('/gitlab/gitlabhq/-/tags/feature%2345')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45')
+ expect(delete('/gitlab/gitlabhq/-/tags/feature%2B45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45')
+ expect(delete('/gitlab/gitlabhq/-/tags/feature@45')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45')
+ expect(delete('/gitlab/gitlabhq/-/tags/feature%2345/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature#45/foo/bar/baz')
+ expect(delete('/gitlab/gitlabhq/-/tags/feature%2B45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature+45/foo/bar/baz')
+ expect(delete('/gitlab/gitlabhq/-/tags/feature@45/foo/bar/baz')).to route_to('projects/tags#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'feature@45/foo/bar/baz')
end
end
@@ -237,7 +241,10 @@ describe 'project routing' do
it_behaves_like 'RESTful project resources' do
let(:actions) { [:index, :new, :create, :edit, :update] }
let(:controller) { 'deploy_keys' }
+ let(:controller_path) { '/-/deploy_keys' }
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/deploy_keys", "/gitlab/gitlabhq/-/deploy_keys"
end
# project_protected_branches GET /:project_id/protected_branches(.:format) protected_branches#index
@@ -247,6 +254,7 @@ describe 'project routing' do
it_behaves_like 'RESTful project resources' do
let(:actions) { [:index, :create, :destroy] }
let(:controller) { 'protected_branches' }
+ let(:controller_path) { '/-/protected_branches' }
end
end
@@ -444,7 +452,10 @@ describe 'project routing' do
it_behaves_like 'RESTful project resources' do
let(:actions) { [:index, :create, :update, :destroy] }
let(:controller) { 'project_members' }
+ let(:controller_path) { '/-/project_members' }
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/project_members", "/gitlab/gitlabhq/-/project_members"
end
# project_milestones GET /:project_id/milestones(.:format) milestones#index
@@ -459,18 +470,23 @@ describe 'project routing' do
it_behaves_like 'RESTful project resources' do
let(:controller) { 'milestones' }
let(:actions) { [:index, :create, :new, :edit, :show, :update] }
+ let(:controller_path) { '/-/milestones' }
end
it 'to #promote' do
- expect(post('/gitlab/gitlabhq/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
+ expect(post('/gitlab/gitlabhq/-/milestones/1/promote')).to route_to('projects/milestones#promote', namespace_id: 'gitlab', project_id: 'gitlabhq', id: "1")
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/milestones", "/gitlab/gitlabhq/-/milestones"
end
# project_labels GET /:project_id/labels(.:format) labels#index
describe Projects::LabelsController, 'routing' do
it 'to #index' do
- expect(get('/gitlab/gitlabhq/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq/-/labels')).to route_to('projects/labels#index', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/labels", "/gitlab/gitlabhq/-/labels"
end
# sort_project_issues POST /:project_id/issues/sort(.:format) issues#sort
@@ -592,36 +608,44 @@ describe 'project routing' do
describe Projects::NetworkController, 'routing' do
it 'to #show' do
- expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
- expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
- expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ expect(get('/gitlab/gitlabhq/-/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ expect(get('/gitlab/gitlabhq/-/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
+ expect(get('/gitlab/gitlabhq/-/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/network/master", "/gitlab/gitlabhq/-/network/master"
end
describe Projects::GraphsController, 'routing' do
it 'to #show' do
- expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
- expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
- expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ expect(get('/gitlab/gitlabhq/-/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ expect(get('/gitlab/gitlabhq/-/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json')
+ expect(get('/gitlab/gitlabhq/-/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/graphs/master", "/gitlab/gitlabhq/-/graphs/master"
end
describe Projects::ForksController, 'routing' do
it 'to #new' do
- expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq/-/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
it 'to #create' do
- expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ expect(post('/gitlab/gitlabhq/-/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/forks", "/gitlab/gitlabhq/-/forks"
end
# project_avatar DELETE /project/avatar(.:format) projects/avatars#destroy
describe Projects::AvatarsController, 'routing' do
it 'to #destroy' do
- expect(delete('/gitlab/gitlabhq/avatar')).to route_to(
+ expect(delete('/gitlab/gitlabhq/-/avatar')).to route_to(
'projects/avatars#destroy', namespace_id: 'gitlab', project_id: 'gitlabhq')
end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/avatar", "/gitlab/gitlabhq/-/avatar"
end
describe Projects::PagesDomainsController, 'routing' do
@@ -661,4 +685,12 @@ describe 'project routing' do
end
end
end
+
+ describe Projects::Settings::RepositoryController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/-/settings/repository')).to route_to('projects/settings/repository#show', namespace_id: 'gitlab', project_id: 'gitlabhq')
+ end
+
+ it_behaves_like 'redirecting a legacy project path', "/gitlab/gitlabhq/settings/repository", "/gitlab/gitlabhq/-/settings/repository"
+ end
end
diff --git a/spec/routing/uploads_routing_spec.rb b/spec/routing/uploads_routing_spec.rb
new file mode 100644
index 00000000000..6a041ffdd6c
--- /dev/null
+++ b/spec/routing/uploads_routing_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'Uploads', 'routing' do
+ it 'allows creating uploads for personal snippets' do
+ expect(post('/uploads/personal_snippet?id=1')).to route_to(
+ controller: 'uploads',
+ action: 'create',
+ model: 'personal_snippet',
+ id: '1'
+ )
+ end
+
+ it 'does not allow creating uploads for other models' do
+ UploadsController::MODEL_CLASSES.keys.compact.each do |model|
+ next if model == 'personal_snippet'
+
+ expect(post("/uploads/#{model}?id=1")).not_to be_routable
+ end
+ end
+end
diff --git a/spec/rubocop/cop/active_record_association_reload_spec.rb b/spec/rubocop/cop/active_record_association_reload_spec.rb
new file mode 100644
index 00000000000..69eb16a54d2
--- /dev/null
+++ b/spec/rubocop/cop/active_record_association_reload_spec.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require 'rubocop'
+require_relative '../../../rubocop/cop/active_record_association_reload'
+
+describe RuboCop::Cop::ActiveRecordAssociationReload do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when using ActiveRecord::Base' do
+ it 'registers an offense on reload usage' do
+ expect_offense(<<~PATTERN.strip_indent)
+ users = User.all
+ users.reload
+ ^^^^^^ Use reset instead of reload. For more details check the https://gitlab.com/gitlab-org/gitlab-ce/issues/60218.
+ PATTERN
+ end
+
+ it 'does not register an offense on reset usage' do
+ expect_no_offenses(<<~PATTERN.strip_indent)
+ users = User.all
+ users.reset
+ PATTERN
+ end
+ end
+
+ context 'when using ActiveRecord::Relation' do
+ it 'registers an offense on reload usage' do
+ expect_offense(<<~PATTERN.strip_indent)
+ user = User.new
+ user.reload
+ ^^^^^^ Use reset instead of reload. For more details check the https://gitlab.com/gitlab-org/gitlab-ce/issues/60218.
+ PATTERN
+ end
+
+ it 'does not register an offense on reset usage' do
+ expect_no_offenses(<<~PATTERN.strip_indent)
+ user = User.new
+ user.reset
+ PATTERN
+ end
+ end
+
+ context 'when using on self' do
+ it 'registers an offense on reload usage' do
+ expect_offense(<<~PATTERN.strip_indent)
+ reload
+ ^^^^^^ Use reset instead of reload. For more details check the https://gitlab.com/gitlab-org/gitlab-ce/issues/60218.
+ PATTERN
+ end
+
+ it 'does not register an offense on reset usage' do
+ expect_no_offenses(<<~PATTERN.strip_indent)
+ reset
+ PATTERN
+ end
+ end
+end
diff --git a/spec/rubocop/cop/code_reuse/active_record_spec.rb b/spec/rubocop/cop/code_reuse/active_record_spec.rb
index a30fc52d26f..8f3a3690d88 100644
--- a/spec/rubocop/cop/code_reuse/active_record_spec.rb
+++ b/spec/rubocop/cop/code_reuse/active_record_spec.rb
@@ -14,7 +14,7 @@ describe RuboCop::Cop::CodeReuse::ActiveRecord do
expect_offense(<<~SOURCE)
def foo
User.where
- ^^^^^ This method can only be used inside an ActiveRecord model
+ ^^^^^ This method can only be used inside an ActiveRecord model: https://gitlab.com/gitlab-org/gitlab-ce/issues/49653
end
SOURCE
end
@@ -23,7 +23,7 @@ describe RuboCop::Cop::CodeReuse::ActiveRecord do
expect_offense(<<~SOURCE)
def foo
User.where(id: 10)
- ^^^^^ This method can only be used inside an ActiveRecord model
+ ^^^^^ This method can only be used inside an ActiveRecord model: https://gitlab.com/gitlab-org/gitlab-ce/issues/49653
end
SOURCE
end
@@ -40,7 +40,7 @@ describe RuboCop::Cop::CodeReuse::ActiveRecord do
expect_offense(<<~SOURCE)
def foo
project.group(:name)
- ^^^^^ This method can only be used inside an ActiveRecord model
+ ^^^^^ This method can only be used inside an ActiveRecord model: https://gitlab.com/gitlab-org/gitlab-ce/issues/49653
end
SOURCE
end
diff --git a/spec/rubocop/cop/include_action_view_context_spec.rb b/spec/rubocop/cop/include_action_view_context_spec.rb
new file mode 100644
index 00000000000..c888555b54f
--- /dev/null
+++ b/spec/rubocop/cop/include_action_view_context_spec.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+require 'rubocop'
+require 'rubocop/rspec/support'
+
+require_relative '../../../rubocop/cop/include_action_view_context'
+
+describe RuboCop::Cop::IncludeActionViewContext do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'when `ActionView::Context` is included' do
+ let(:source) { 'include ActionView::Context' }
+ let(:correct_source) { 'include ::Gitlab::ActionViewOutput::Context' }
+
+ it 'registers an offense' do
+ inspect_source(source)
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ expect(cop.highlights).to eq(['ActionView::Context'])
+ end
+ end
+
+ it 'autocorrects to the right version' do
+ autocorrected = autocorrect_source(source)
+
+ expect(autocorrected).to eq(correct_source)
+ end
+ end
+
+ context 'when `ActionView::Context` is not included' do
+ it 'registers no offense' do
+ inspect_source('include Context')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
index 1c8ab0ad5d2..cba01400d85 100644
--- a/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
+++ b/spec/rubocop/cop/migration/update_column_in_batches_spec.rb
@@ -93,4 +93,22 @@ describe RuboCop::Cop::Migration::UpdateColumnInBatches do
it_behaves_like 'a migration file with no spec file'
it_behaves_like 'a migration file with a spec file'
end
+
+ context 'EE migrations' do
+ let(:spec_filepath) { tmp_rails_root.join('ee', 'spec', 'migrations', 'my_super_migration_spec.rb') }
+
+ context 'in a migration' do
+ let(:migration_filepath) { tmp_rails_root.join('ee', 'db', 'migrate', '20121220064453_my_super_migration.rb') }
+
+ it_behaves_like 'a migration file with no spec file'
+ it_behaves_like 'a migration file with a spec file'
+ end
+
+ context 'in a post migration' do
+ let(:migration_filepath) { tmp_rails_root.join('ee', 'db', 'post_migrate', '20121220064453_my_super_migration.rb') }
+
+ it_behaves_like 'a migration file with no spec file'
+ it_behaves_like 'a migration file with a spec file'
+ end
+ end
end
diff --git a/spec/rubocop/cop/qa/element_with_pattern_spec.rb b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
index c5beb40f9fd..ef20d9a1f26 100644
--- a/spec/rubocop/cop/qa/element_with_pattern_spec.rb
+++ b/spec/rubocop/cop/qa/element_with_pattern_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'rubocop'
@@ -23,7 +25,7 @@ describe RuboCop::Cop::QA::ElementWithPattern do
element :groups_filter, 'search_field_tag :filter'
^^^^^^^^^^^^^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter` instead.
element :groups_filter_placeholder, /Search by name/
- ^^^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter-placeholder` instead.
+ ^^^^^^^^^^^^^^ Don't use a pattern for element, create a corresponding `qa-groups-filter-placeholder` instead.
end
RUBY
end
@@ -35,6 +37,13 @@ describe RuboCop::Cop::QA::ElementWithPattern do
element :groups_filter_placeholder
end
RUBY
+
+ expect_no_offenses(<<-RUBY)
+ view 'app/views/shared/groups/_search_form.html.haml' do
+ element :groups_filter, required: true
+ element :groups_filter_placeholder, required: false
+ end
+ RUBY
end
end
diff --git a/spec/serializers/analytics_stage_serializer_spec.rb b/spec/serializers/analytics_stage_serializer_spec.rb
index be6aa7c65c3..5b05c2f2ef3 100644
--- a/spec/serializers/analytics_stage_serializer_spec.rb
+++ b/spec/serializers/analytics_stage_serializer_spec.rb
@@ -14,11 +14,41 @@ describe AnalyticsStageSerializer do
allow_any_instance_of(Gitlab::CycleAnalytics::BaseEventFetcher).to receive(:event_result).and_return({})
end
- it 'it generates payload for single object' do
+ it 'generates payload for single object' do
expect(subject).to be_kind_of Hash
end
it 'contains important elements of AnalyticsStage' do
expect(subject).to include(:title, :description, :value)
end
+
+ context 'when median is equal 0' do
+ before do
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(0)
+ end
+
+ it 'sets the value to nil' do
+ expect(subject.fetch(:value)).to be_nil
+ end
+ end
+
+ context 'when median is below 1' do
+ before do
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(0.12)
+ end
+
+ it 'sets the value to equal to median' do
+ expect(subject.fetch(:value)).to eq('less than a minute')
+ end
+ end
+
+ context 'when median is above 1' do
+ before do
+ allow_any_instance_of(Gitlab::CycleAnalytics::BaseStage).to receive(:median).and_return(60.12)
+ end
+
+ it 'sets the value to equal to median' do
+ expect(subject.fetch(:value)).to eq('1 minute')
+ end
+ end
end
diff --git a/spec/serializers/analytics_summary_serializer_spec.rb b/spec/serializers/analytics_summary_serializer_spec.rb
index 236c244b402..8fa0574bfd6 100644
--- a/spec/serializers/analytics_summary_serializer_spec.rb
+++ b/spec/serializers/analytics_summary_serializer_spec.rb
@@ -18,7 +18,7 @@ describe AnalyticsSummarySerializer do
.to receive(:value).and_return(1.12)
end
- it 'it generates payload for single object' do
+ it 'generates payload for single object' do
expect(subject).to be_kind_of Hash
end
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index f6bd6e9ede4..d922e8246c7 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -112,5 +112,48 @@ describe BuildDetailsEntity do
expect(subject['merge_request_path']).to be_nil
end
end
+
+ context 'when the build has failed' do
+ let(:build) { create(:ci_build, :created) }
+
+ before do
+ build.drop!(:unmet_prerequisites)
+ end
+
+ it { is_expected.to include(failure_reason: 'unmet_prerequisites') }
+ end
+
+ context 'when a build has environment with latest deployment' do
+ let(:build) do
+ create(:ci_build, :running, environment: environment.name, pipeline: pipeline)
+ end
+
+ let(:environment) do
+ create(:environment, project: project, name: 'staging', state: :available)
+ end
+
+ before do
+ create(:deployment, :success, environment: environment, project: project)
+
+ allow(request).to receive(:project).and_return(project)
+ end
+
+ it 'does not serialize latest deployment commit and associated builds' do
+ response = subject.with_indifferent_access
+
+ response.dig(:deployment_status, :environment, :last_deployment).tap do |deployment|
+ expect(deployment).not_to include(:commit, :manual_actions, :scheduled_actions)
+ end
+ end
+ end
+
+ context 'when the build has reports' do
+ let!(:report) { create(:ci_job_artifact, :codequality, job: build) }
+
+ it 'exposes the report artifacts' do
+ expect(subject[:reports].count).to eq(1)
+ expect(subject[:reports].first[:file_type]).to eq('codequality')
+ end
+ end
end
end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index 7e151c3744e..f38a18fcf59 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -21,6 +21,10 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to be_nil
end
+ it 'has can_uninstall' do
+ expect(subject[:can_uninstall]).to be_falsey
+ end
+
context 'non-helm application' do
let(:application) { build(:clusters_applications_runner, version: '0.0.0') }
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index 894fd7a0a12..76ad2aee5c5 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -10,6 +10,7 @@ describe DeploymentEntity do
let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
let(:pipeline) { create(:ci_pipeline, project: project, user: user) }
let(:entity) { described_class.new(deployment, request: request) }
+
subject { entity.as_json }
before do
@@ -47,6 +48,16 @@ describe DeploymentEntity do
expect(subject[:manual_actions]).not_to be_present
end
end
+
+ context 'when deployment details serialization was disabled' do
+ let(:entity) do
+ described_class.new(deployment, request: request, deployment_details: false)
+ end
+
+ it 'does not serialize manual actions details' do
+ expect(subject.with_indifferent_access).not_to include(:manual_actions)
+ end
+ end
end
describe 'scheduled_actions' do
@@ -69,5 +80,35 @@ describe DeploymentEntity do
expect(subject[:scheduled_actions]).to be_empty
end
end
+
+ context 'when deployment details serialization was disabled' do
+ let(:entity) do
+ described_class.new(deployment, request: request, deployment_details: false)
+ end
+
+ it 'does not serialize scheduled actions details' do
+ expect(subject.with_indifferent_access).not_to include(:scheduled_actions)
+ end
+ end
+ end
+
+ context 'when deployment details serialization was disabled' do
+ include Gitlab::Routing
+
+ let(:entity) do
+ described_class.new(deployment, request: request, deployment_details: false)
+ end
+
+ it 'does not serialize deployment details' do
+ expect(subject.with_indifferent_access)
+ .not_to include(:commit, :manual_actions, :scheduled_actions)
+ end
+
+ it 'only exposes deployable name and path' do
+ project_job_path(project, deployment.deployable).tap do |path|
+ expect(subject.fetch(:deployable))
+ .to eq(name: 'test', build_path: path)
+ end
+ end
end
end
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 791b64dc356..c2312734042 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -54,7 +54,7 @@ describe EnvironmentEntity do
projects: [project])
end
- it 'should include cluster_type' do
+ it 'includes cluster_type' do
expect(subject).to include(:cluster_type)
expect(subject[:cluster_type]).to eq('project_type')
end
@@ -65,7 +65,7 @@ describe EnvironmentEntity do
create(:kubernetes_service, project: project)
end
- it 'should not include cluster_type' do
+ it 'does not include cluster_type' do
expect(subject).not_to include(:cluster_type)
end
end
diff --git a/spec/serializers/group_child_entity_spec.rb b/spec/serializers/group_child_entity_spec.rb
index d02b4c554b1..b58d95ccb43 100644
--- a/spec/serializers/group_child_entity_spec.rb
+++ b/spec/serializers/group_child_entity_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe GroupChildEntity do
+ include ExternalAuthorizationServiceHelpers
include Gitlab::Routing.url_helpers
let(:user) { create(:user) }
@@ -109,4 +110,22 @@ describe GroupChildEntity do
it_behaves_like 'group child json'
end
+
+ describe 'for a project with external authorization enabled' do
+ let(:object) do
+ create(:project, :with_avatar,
+ description: 'Awesomeness')
+ end
+
+ before do
+ enable_external_authorization_service_check
+ object.add_maintainer(user)
+ end
+
+ it 'does not hit the external authorization service' do
+ expect(::Gitlab::ExternalAuthorization).not_to receive(:access_allowed?)
+
+ expect(json[:can_edit]).to eq(false)
+ end
+ end
end
diff --git a/spec/serializers/job_artifact_report_entity_spec.rb b/spec/serializers/job_artifact_report_entity_spec.rb
new file mode 100644
index 00000000000..eef5c16d0fb
--- /dev/null
+++ b/spec/serializers/job_artifact_report_entity_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe JobArtifactReportEntity do
+ let(:report) { create(:ci_job_artifact, :codequality) }
+ let(:entity) { described_class.new(report, request: double) }
+
+ describe '#as_json' do
+ subject { entity.as_json }
+
+ it 'exposes file_type' do
+ expect(subject[:file_type]).to eq(report.file_type)
+ end
+
+ it 'exposes file_format' do
+ expect(subject[:file_format]).to eq(report.file_format)
+ end
+
+ it 'exposes size' do
+ expect(subject[:size]).to eq(report.size)
+ end
+
+ it 'exposes download path' do
+ expect(subject[:download_path]).to include("jobs/#{report.job.id}/artifacts/download")
+ end
+ end
+end
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 851b41a7f7e..8de61d4d466 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -154,15 +154,15 @@ describe JobEntity do
expect(subject[:status][:label]).to eq('failed')
end
- it 'should indicate the failure reason on tooltip' do
+ it 'indicates the failure reason on tooltip' do
expect(subject[:status][:tooltip]).to eq('failed - (API failure)')
end
- it 'should include a callout message with a verbose output' do
+ it 'includes a callout message with a verbose output' do
expect(subject[:callout_message]).to eq('There has been an API failure, please try again')
end
- it 'should state that it is not recoverable' do
+ it 'states that it is not recoverable' do
expect(subject[:recoverable]).to be_truthy
end
end
@@ -178,15 +178,15 @@ describe JobEntity do
expect(subject[:status][:label]).to eq('failed (allowed to fail)')
end
- it 'should indicate the failure reason on tooltip' do
+ it 'indicates the failure reason on tooltip' do
expect(subject[:status][:tooltip]).to eq('failed - (API failure) (allowed to fail)')
end
- it 'should include a callout message with a verbose output' do
+ it 'includes a callout message with a verbose output' do
expect(subject[:callout_message]).to eq('There has been an API failure, please try again')
end
- it 'should state that it is not recoverable' do
+ it 'states that it is not recoverable' do
expect(subject[:recoverable]).to be_truthy
end
end
@@ -194,7 +194,7 @@ describe JobEntity do
context 'when the job failed with a script failure' do
let(:job) { create(:ci_build, :failed, :script_failure) }
- it 'should not include callout message or recoverable keys' do
+ it 'does not include callout message or recoverable keys' do
expect(subject).not_to include('callout_message')
expect(subject).not_to include('recoverable')
end
@@ -203,7 +203,7 @@ describe JobEntity do
context 'when job failed and is recoverable' do
let(:job) { create(:ci_build, :api_failure) }
- it 'should state it is recoverable' do
+ it 'states it is recoverable' do
expect(subject[:recoverable]).to be_truthy
end
end
@@ -211,7 +211,7 @@ describe JobEntity do
context 'when job passed' do
let(:job) { create(:ci_build, :success) }
- it 'should not include callout message or recoverable keys' do
+ it 'does not include callout message or recoverable keys' do
expect(subject).not_to include('callout_message')
expect(subject).not_to include('recoverable')
end
diff --git a/spec/serializers/merge_request_for_pipeline_entity_spec.rb b/spec/serializers/merge_request_for_pipeline_entity_spec.rb
new file mode 100644
index 00000000000..e49b45bc7d7
--- /dev/null
+++ b/spec/serializers/merge_request_for_pipeline_entity_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe MergeRequestForPipelineEntity do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:request) { EntityRequest.new(project: project) }
+ let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
+ let(:presenter) { MergeRequestPresenter.new(merge_request, current_user: user) }
+
+ let(:entity) do
+ described_class.new(presenter, request: request)
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'as json' do
+ subject { entity.as_json }
+
+ it 'exposes needed attributes' do
+ expect(subject).to include(
+ :iid, :path, :title,
+ :source_branch, :source_branch_path,
+ :target_branch, :target_branch_path
+ )
+ end
+ end
+end
diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb
index 4dbd79f2fc0..a27c22191f4 100644
--- a/spec/serializers/merge_request_widget_entity_spec.rb
+++ b/spec/serializers/merge_request_widget_entity_spec.rb
@@ -13,6 +13,10 @@ describe MergeRequestWidgetEntity do
described_class.new(resource, request: request).as_json
end
+ it 'has the latest sha of the target branch' do
+ is_expected.to include(:target_branch_sha)
+ end
+
describe 'source_project_full_path' do
it 'includes the full path of the source project' do
expect(subject[:source_project_full_path]).to be_present
@@ -279,13 +283,64 @@ describe MergeRequestWidgetEntity do
end
describe 'commits_without_merge_commits' do
- it 'should not include merge commits' do
- # Mock all but the first 5 commits to be merge commits
- resource.commits.each_with_index do |commit, i|
- expect(commit).to receive(:merge_commit?).at_least(:once).and_return(i > 4)
+ def find_matching_commit(short_id)
+ resource.commits.find { |c| c.short_id == short_id }
+ end
+
+ it 'does not include merge commits' do
+ commits_in_widget = subject[:commits_without_merge_commits]
+
+ expect(commits_in_widget.length).to be < resource.commits.length
+ expect(commits_in_widget.length).to eq(resource.commits.without_merge_commits.length)
+ commits_in_widget.each do |c|
+ expect(find_matching_commit(c[:short_id]).merge_commit?).to eq(false)
end
+ end
+ end
- expect(subject[:commits_without_merge_commits].size).to eq(5)
+ describe 'auto merge' do
+ context 'when auto merge is enabled' do
+ let(:resource) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_enabled]).to be_truthy
+ expect(subject[:auto_merge_strategy]).to eq('merge_when_pipeline_succeeds')
+ end
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:resource) { create(:merge_request) }
+
+ it 'returns auto merge related information' do
+ expect(subject[:auto_merge_enabled]).to be_falsy
+ expect(subject[:auto_merge_strategy]).to be_nil
+ end
+ end
+
+ context 'when head pipeline is running' do
+ before do
+ create(:ci_pipeline, :running, project: project,
+ ref: resource.source_branch,
+ sha: resource.diff_head_sha)
+ resource.update_head_pipeline
+ end
+
+ it 'returns available auto merge strategies' do
+ expect(subject[:available_auto_merge_strategies]).to eq(%w[merge_when_pipeline_succeeds])
+ end
+ end
+
+ context 'when head pipeline is finished' do
+ before do
+ create(:ci_pipeline, :success, project: project,
+ ref: resource.source_branch,
+ sha: resource.diff_head_sha)
+ resource.update_head_pipeline
+ end
+
+ it 'returns available auto merge strategies' do
+ expect(subject[:available_auto_merge_strategies]).to be_empty
+ end
end
end
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 774486dcb6d..6be612ec226 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -1,13 +1,18 @@
require 'spec_helper'
describe PipelineEntity do
+ include Gitlab::Routing
+
+ set(:project) { create(:project) }
set(:user) { create(:user) }
+ set(:project) { create(:project) }
let(:request) { double('request') }
before do
stub_not_protect_default_branch
allow(request).to receive(:current_user).and_return(user)
+ allow(request).to receive(:project).and_return(project)
end
let(:entity) do
@@ -43,8 +48,8 @@ describe PipelineEntity do
it 'contains flags' do
expect(subject).to include :flags
expect(subject[:flags])
- .to include :latest, :stuck, :auto_devops,
- :yaml_errors, :retryable, :cancelable, :merge_request
+ .to include :stuck, :auto_devops, :yaml_errors,
+ :retryable, :cancelable, :merge_request
end
end
@@ -59,6 +64,12 @@ describe PipelineEntity do
create(:ci_build, :failed, pipeline: pipeline)
end
+ it 'does not serialize stage builds' do
+ subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
+ expect(stage).not_to include(:groups, :latest_statuses, :retries)
+ end
+ end
+
context 'user has ability to retry pipeline' do
before do
project.add_developer(user)
@@ -87,6 +98,12 @@ describe PipelineEntity do
create(:ci_build, :pending, pipeline: pipeline)
end
+ it 'does not serialize stage builds' do
+ subject.with_indifferent_access.dig(:details, :stages, 0).tap do |stage|
+ expect(stage).not_to include(:groups, :latest_statuses, :retries)
+ end
+ end
+
context 'user has ability to cancel pipeline' do
before do
project.add_developer(user)
@@ -128,5 +145,72 @@ describe PipelineEntity do
.to eq 'CI/CD YAML configuration error!'
end
end
+
+ context 'when pipeline is detached merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_detached_merge_request_pipeline) }
+ let(:project) { merge_request.target_project }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+
+ it 'makes detached flag true' do
+ expect(subject[:flags][:detached_merge_request_pipeline]).to be_truthy
+ end
+
+ it 'does not expose source sha and target sha' do
+ expect(subject[:source_sha]).to be_nil
+ expect(subject[:target_sha]).to be_nil
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'has merge request information' do
+ expect(subject[:merge_request][:iid]).to eq(merge_request.iid)
+
+ expect(project_merge_request_path(project, merge_request))
+ .to include(subject[:merge_request][:path])
+
+ expect(subject[:merge_request][:title]).to eq(merge_request.title)
+
+ expect(subject[:merge_request][:source_branch])
+ .to eq(merge_request.source_branch)
+
+ expect(project_commits_path(project, merge_request.source_branch))
+ .to include(subject[:merge_request][:source_branch_path])
+
+ expect(subject[:merge_request][:target_branch])
+ .to eq(merge_request.target_branch)
+
+ expect(project_commits_path(project, merge_request.target_branch))
+ .to include(subject[:merge_request][:target_branch_path])
+ end
+ end
+
+ context 'when user is an external user' do
+ it 'has no merge request information' do
+ expect(subject[:merge_request]).to be_nil
+ end
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:merge_request) { create(:merge_request, :with_merge_request_pipeline, merge_sha: 'abc') }
+ let(:project) { merge_request.target_project }
+ let(:pipeline) { merge_request.pipelines_for_merge_request.first }
+
+ it 'makes detached flag false' do
+ expect(subject[:flags][:detached_merge_request_pipeline]).to be_falsy
+ end
+
+ it 'makes atached flag true' do
+ expect(subject[:flags][:merge_request_pipeline]).to be_truthy
+ end
+
+ it 'exposes source sha and target sha' do
+ expect(subject[:source_sha]).to be_present
+ expect(subject[:target_sha]).to be_present
+ end
+ end
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 2bdcb2a45f6..54e6abc2d3a 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -5,7 +5,7 @@ describe PipelineSerializer do
set(:user) { create(:user) }
let(:serializer) do
- described_class.new(current_user: user)
+ described_class.new(current_user: user, project: project)
end
before do
@@ -97,6 +97,44 @@ describe PipelineSerializer do
end
end
+ context 'when there are pipelines for merge requests' do
+ let(:resource) { Ci::Pipeline.all }
+
+ let!(:merge_request_1) do
+ create(:merge_request,
+ :with_detached_merge_request_pipeline,
+ target_project: project,
+ target_branch: 'master',
+ source_project: project,
+ source_branch: 'feature')
+ end
+
+ let!(:merge_request_2) do
+ create(:merge_request,
+ :with_detached_merge_request_pipeline,
+ target_project: project,
+ target_branch: 'master',
+ source_project: project,
+ source_branch: '2-mb-file')
+ end
+
+ before do
+ project.add_developer(user)
+ end
+
+ it 'includes merge requests information' do
+ expect(subject.all? { |entry| entry[:merge_request].present? }).to be_truthy
+ end
+
+ it 'preloads related merge requests', :postgresql do
+ recorded = ActiveRecord::QueryRecorder.new { subject }
+
+ expect(recorded.log)
+ .to include("SELECT \"merge_requests\".* FROM \"merge_requests\" " \
+ "WHERE \"merge_requests\".\"id\" IN (#{merge_request_1.id}, #{merge_request_2.id})")
+ end
+ end
+
describe 'number of queries when preloaded' do
subject { serializer.represent(resource, preload: true) }
let(:resource) { Ci::Pipeline.all }
@@ -118,8 +156,9 @@ describe PipelineSerializer do
it 'verifies number of queries', :request_store do
recorded = ActiveRecord::QueryRecorder.new { subject }
+ expected_queries = Gitlab.ee? ? 38 : 31
- expect(recorded.count).to be_within(2).of(31)
+ expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
end
end
@@ -138,7 +177,8 @@ describe PipelineSerializer do
# pipeline. With the same ref this check is cached but if refs are
# different then there is an extra query per ref
# https://gitlab.com/gitlab-org/gitlab-ce/issues/46368
- expect(recorded.count).to be_within(2).of(38)
+ expected_queries = Gitlab.ee? ? 44 : 38
+ expect(recorded.count).to be_within(2).of(expected_queries)
expect(recorded.cached_count).to eq(0)
end
end
diff --git a/spec/serializers/provider_repo_entity_spec.rb b/spec/serializers/provider_repo_entity_spec.rb
index b67115bab10..9a1160d16d5 100644
--- a/spec/serializers/provider_repo_entity_spec.rb
+++ b/spec/serializers/provider_repo_entity_spec.rb
@@ -13,7 +13,7 @@ describe ProviderRepoEntity do
describe '#as_json' do
subject { entity.as_json }
- it 'includes requried fields' do
+ it 'includes required fields' do
expect(subject[:id]).to eq(provider_repo[:id])
expect(subject[:full_name]).to eq(provider_repo[:full_name])
expect(subject[:owner_name]).to eq(provider_repo[:owner][:login])
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
index 2034c7891ef..6b1185d1283 100644
--- a/spec/serializers/stage_entity_spec.rb
+++ b/spec/serializers/stage_entity_spec.rb
@@ -48,6 +48,10 @@ describe StageEntity do
expect(subject[:title]).to eq 'test: passed'
end
+ it 'does not contain play_details info' do
+ expect(subject[:status][:action]).not_to be_present
+ end
+
context 'when the jobs should be grouped' do
let(:entity) { described_class.new(stage, request: request, grouped: true) }
@@ -66,5 +70,29 @@ describe StageEntity do
end
end
end
+
+ context 'with a skipped stage ' do
+ let(:stage) { create(:ci_stage_entity, status: 'skipped') }
+
+ it 'contains play_all_manual' do
+ expect(subject[:status][:action]).to be_present
+ end
+ end
+
+ context 'with a scheduled stage ' do
+ let(:stage) { create(:ci_stage_entity, status: 'scheduled') }
+
+ it 'contains play_all_manual' do
+ expect(subject[:status][:action]).to be_present
+ end
+ end
+
+ context 'with a manual stage ' do
+ let(:stage) { create(:ci_stage_entity, status: 'manual') }
+
+ it 'contains play_all_manual' do
+ expect(subject[:status][:action]).to be_present
+ end
+ end
end
end
diff --git a/spec/serializers/stage_serializer_spec.rb b/spec/serializers/stage_serializer_spec.rb
new file mode 100644
index 00000000000..aae17cfbcb9
--- /dev/null
+++ b/spec/serializers/stage_serializer_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe StageSerializer do
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:resource) { create(:ci_stage_entity) }
+
+ let(:serializer) do
+ described_class.new(current_user: user, project: project)
+ end
+
+ subject { serializer.represent(resource) }
+
+ describe '#represent' do
+ context 'with a single entity' do
+ it 'serializes the stage object' do
+ expect(subject[:name]).to eq(resource.name)
+ end
+ end
+
+ context 'with an array of entities' do
+ let(:resource) { create_list(:ci_stage_entity, 2) }
+
+ it 'serializes the array of pipelines' do
+ expect(subject).not_to be_empty
+ end
+ end
+ end
+end
diff --git a/spec/serializers/suggestion_entity_spec.rb b/spec/serializers/suggestion_entity_spec.rb
index 047571f161c..d282a7f9c7a 100644
--- a/spec/serializers/suggestion_entity_spec.rb
+++ b/spec/serializers/suggestion_entity_spec.rb
@@ -13,8 +13,7 @@ describe SuggestionEntity do
subject { entity.as_json }
it 'exposes correct attributes' do
- expect(subject).to include(:id, :from_original_line, :to_original_line, :from_line,
- :to_line, :appliable, :applied, :from_content, :to_content)
+ expect(subject.keys).to match_array([:id, :appliable, :applied, :diff_lines, :current_user])
end
it 'exposes current user abilities' do
diff --git a/spec/serializers/test_case_entity_spec.rb b/spec/serializers/test_case_entity_spec.rb
index a55910f98bb..986c9feb07b 100644
--- a/spec/serializers/test_case_entity_spec.rb
+++ b/spec/serializers/test_case_entity_spec.rb
@@ -14,6 +14,7 @@ describe TestCaseEntity do
it 'contains correct test case details' do
expect(subject[:status]).to eq('success')
expect(subject[:name]).to eq('Test#sum when a is 1 and b is 3 returns summary')
+ expect(subject[:classname]).to eq('spec.test_spec')
expect(subject[:execution_time]).to eq(1.11)
end
end
@@ -24,6 +25,7 @@ describe TestCaseEntity do
it 'contains correct test case details' do
expect(subject[:status]).to eq('failed')
expect(subject[:name]).to eq('Test#sum when a is 2 and b is 2 returns summary')
+ expect(subject[:classname]).to eq('spec.test_spec')
expect(subject[:execution_time]).to eq(2.22)
end
end
diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb
index 38a3f522504..b2a8da6c4c6 100644
--- a/spec/services/access_token_validation_service_spec.rb
+++ b/spec/services/access_token_validation_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AccessTokenValidationService do
diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb
deleted file mode 100644
index bc9747d1413..00000000000
--- a/spec/services/after_branch_delete_service_spec.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'spec_helper'
-
-describe AfterBranchDeleteService do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
- let(:service) { described_class.new(project, user) }
-
- describe '#execute' do
- it 'stops environments attached to branch' do
- expect(service).to receive(:stop_environments)
-
- service.execute('feature')
- end
- end
-end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index a4a733eff77..a641828faa5 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ApplicationSettings::UpdateService do
+ include ExternalAuthorizationServiceHelpers
+
let(:application_settings) { create(:application_setting) }
let(:admin) { create(:user, :admin) }
let(:params) { {} }
@@ -143,4 +147,37 @@ describe ApplicationSettings::UpdateService do
end
end
end
+
+ context 'when external authorization is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not save the settings with an error if the service denies access' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(admin, 'new-label') { false }
+
+ described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute
+
+ expect(application_settings.errors[:external_authorization_service_default_label]).to be_present
+ end
+
+ it 'saves the setting when the user has access to the label' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(admin, 'new-label') { true }
+
+ described_class.new(application_settings, admin, { external_authorization_service_default_label: 'new-label' }).execute
+
+ # Read the attribute directly to avoid the stub from
+ # `enable_external_authorization_service_check`
+ expect(application_settings[:external_authorization_service_default_label]).to eq('new-label')
+ end
+
+ it 'does not validate the label if it was not passed' do
+ expect(::Gitlab::ExternalAuthorization)
+ .not_to receive(:access_allowed?)
+
+ described_class.new(application_settings, admin, { home_page_url: 'http://foo.bar' }).execute
+ end
+ end
end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 8021bd338e0..4f4776bbb27 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Auth::ContainerRegistryAuthenticationService do
@@ -88,6 +90,12 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
+ shared_examples 'a deletable since registry 2.7' do
+ it_behaves_like 'an accessible' do
+ let(:actions) { ['delete'] }
+ end
+ end
+
shared_examples 'a pullable' do
it_behaves_like 'an accessible' do
let(:actions) { ['pull'] }
@@ -184,6 +192,19 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory'
end
+ context 'disallow developer to delete images since registry 2.7' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
context 'allow reporter to pull images' do
before do
project.add_reporter(current_user)
@@ -212,6 +233,19 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory'
end
+ context 'disallow reporter to delete images since registry 2.7' do
+ before do
+ project.add_reporter(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
context 'return a least of privileges' do
before do
project.add_reporter(current_user)
@@ -250,6 +284,19 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
it_behaves_like 'not a container repository factory'
end
+
+ context 'disallow guest to delete images since registry 2.7' do
+ before do
+ project.add_guest(current_user)
+ end
+
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
end
context 'for public project' do
@@ -282,6 +329,15 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'not a container repository factory'
end
+ context 'disallow anyone to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
+
context 'when repository name is invalid' do
let(:current_params) do
{ scopes: ['repository:invalid:push'] }
@@ -322,6 +378,15 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
it_behaves_like 'not a container repository factory'
end
+
+ context 'disallow anyone to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
end
context 'for external user' do
@@ -344,6 +409,16 @@ describe Auth::ContainerRegistryAuthenticationService do
it_behaves_like 'an inaccessible'
it_behaves_like 'not a container repository factory'
end
+
+ context 'disallow anyone to delete images since registry 2.7' do
+ let(:current_user) { create(:user, external: true) }
+ let(:current_params) do
+ { scopes: ["repository:#{project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible'
+ it_behaves_like 'not a container repository factory'
+ end
end
end
end
@@ -371,6 +446,16 @@ describe Auth::ContainerRegistryAuthenticationService do
let(:project) { current_project }
end
end
+
+ context 'allow to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'a deletable since registry 2.7' do
+ let(:project) { current_project }
+ end
+ end
end
context 'build authorized as user' do
@@ -419,6 +504,16 @@ describe Auth::ContainerRegistryAuthenticationService do
end
end
+ context 'disallow to delete images since registry 2.7' do
+ let(:current_params) do
+ { scopes: ["repository:#{current_project.full_path}:delete"] }
+ end
+
+ it_behaves_like 'an inaccessible' do
+ let(:project) { current_project }
+ end
+ end
+
context 'for other projects' do
context 'when pulling' do
let(:current_params) do
diff --git a/spec/services/auto_merge/base_service_spec.rb b/spec/services/auto_merge/base_service_spec.rb
new file mode 100644
index 00000000000..197fa16961d
--- /dev/null
+++ b/spec/services/auto_merge/base_service_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AutoMerge::BaseService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user, params) }
+ let(:merge_request) { create(:merge_request) }
+ let(:params) { {} }
+
+ describe '#execute' do
+ subject { service.execute(merge_request) }
+
+ it 'sets properies to the merge request' do
+ subject
+
+ merge_request.reload
+ expect(merge_request).to be_auto_merge_enabled
+ expect(merge_request.merge_user).to eq(user)
+ expect(merge_request.auto_merge_strategy).to eq('base')
+ end
+
+ it 'yields block' do
+ expect { |b| service.execute(merge_request, &b) }.to yield_control.once
+ end
+
+ it 'returns activated strategy name' do
+ is_expected.to eq(:base)
+ end
+
+ context 'when merge parameters are given' do
+ let(:params) do
+ {
+ 'commit_message' => "Merge branch 'patch-12' into 'master'",
+ 'sha' => "200fcc9c260f7219eaf0daba87d818f0922c5b18",
+ 'should_remove_source_branch' => false,
+ 'squash' => false,
+ 'squash_commit_message' => "Update README.md"
+ }
+ end
+
+ it 'sets merge parameters' do
+ subject
+
+ merge_request.reload
+ expect(merge_request.merge_params['commit_message']).to eq("Merge branch 'patch-12' into 'master'")
+ expect(merge_request.merge_params['sha']).to eq('200fcc9c260f7219eaf0daba87d818f0922c5b18')
+ expect(merge_request.merge_params['should_remove_source_branch']).to eq(false)
+ expect(merge_request.merge_params['squash']).to eq(false)
+ expect(merge_request.merge_params['squash_commit_message']).to eq('Update README.md')
+ end
+ end
+
+ context 'when strategy is merge when pipeline succeeds' do
+ let(:service) { AutoMerge::MergeWhenPipelineSucceedsService.new(project, user) }
+
+ it 'sets the auto merge strategy' do
+ subject
+
+ merge_request.reload
+ expect(merge_request.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+ end
+
+ it 'returns activated strategy name' do
+ is_expected.to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS.to_sym)
+ end
+ end
+
+ context 'when failed to save' do
+ before do
+ allow(merge_request).to receive(:save) { false }
+ end
+
+ it 'does not yield block' do
+ expect { |b| service.execute(merge_request, &b) }.not_to yield_control
+ end
+
+ it 'returns failed' do
+ is_expected.to eq(:failed)
+ end
+ end
+ end
+
+ describe '#cancel' do
+ subject { service.cancel(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'removes properies from the merge request' do
+ subject
+
+ merge_request.reload
+ expect(merge_request).not_to be_auto_merge_enabled
+ expect(merge_request.merge_user).to be_nil
+ expect(merge_request.auto_merge_strategy).to be_nil
+ end
+
+ it 'yields block' do
+ expect { |b| service.cancel(merge_request, &b) }.to yield_control.once
+ end
+
+ it 'returns success status' do
+ expect(subject[:status]).to eq(:success)
+ end
+
+ context 'when merge params are set' do
+ before do
+ merge_request.update!(merge_params:
+ {
+ 'should_remove_source_branch' => false,
+ 'commit_message' => "Merge branch 'patch-12' into 'master'",
+ 'squash_commit_message' => "Update README.md",
+ 'auto_merge_strategy' => 'merge_when_pipeline_succeeds'
+ })
+ end
+
+ it 'removes merge parameters' do
+ subject
+
+ merge_request.reload
+ expect(merge_request.merge_params['should_remove_source_branch']).to be_nil
+ expect(merge_request.merge_params['commit_message']).to be_nil
+ expect(merge_request.merge_params['squash_commit_message']).to be_nil
+ expect(merge_request.merge_params['auto_merge_strategy']).to be_nil
+ end
+ end
+
+ context 'when failed to save' do
+ before do
+ allow(merge_request).to receive(:save) { false }
+ end
+
+ it 'does not yield block' do
+ expect { |b| service.execute(merge_request, &b) }.not_to yield_control
+ end
+
+ it 'returns error status' do
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:message]).to eq("Can't cancel the automatic merge")
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
index 52bbd4e794d..a20bf8e17e4 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/auto_merge/merge_when_pipeline_succeeds_service_spec.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe MergeRequests::MergeWhenPipelineSucceedsService do
+describe AutoMerge::MergeWhenPipelineSucceedsService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -19,6 +21,27 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
described_class.new(project, user, commit_message: 'Awesome message')
end
+ describe "#available_for?" do
+ subject { service.available_for?(mr_merge_if_green_enabled) }
+
+ let(:pipeline_status) { :running }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: mr_merge_if_green_enabled.source_branch,
+ sha: mr_merge_if_green_enabled.diff_head_sha,
+ project: mr_merge_if_green_enabled.source_project)
+ mr_merge_if_green_enabled.update_head_pipeline
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
describe "#execute" do
let(:merge_request) do
create(:merge_request, target_project: project, source_project: project,
@@ -28,8 +51,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
context 'first time enabling' do
before do
allow(merge_request)
- .to receive(:head_pipeline)
- .and_return(pipeline)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
service.execute(merge_request)
end
@@ -37,7 +59,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'sets the params, merge_user, and flag' do
expect(merge_request).to be_valid
expect(merge_request.merge_when_pipeline_succeeds).to be_truthy
- expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
+ expect(merge_request.merge_params).to include commit_message: 'Awesome message'
expect(merge_request.merge_user).to be user
end
@@ -52,8 +74,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch) }
before do
- allow(mr_merge_if_green_enabled).to receive(:head_pipeline)
- .and_return(pipeline)
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
allow(mr_merge_if_green_enabled).to receive(:mergeable?)
.and_return(true)
@@ -70,7 +92,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
end
- describe "#trigger" do
+ describe "#process" do
let(:merge_request_ref) { mr_merge_if_green_enabled.source_branch }
let(:merge_request_head) do
project.commit(mr_merge_if_green_enabled.source_branch).id
@@ -84,8 +106,11 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
end
it "merges all merge requests with merge when the pipeline succeeds enabled" do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: triggering_pipeline, actual_head_pipeline: triggering_pipeline)
+
expect(MergeWorker).to receive(:perform_async)
- service.trigger(triggering_pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
@@ -95,9 +120,9 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
sha: '1234abcdef', status: 'success')
end
- it 'it does not merge request' do
+ it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(old_pipeline)
+ service.process(mr_merge_if_green_enabled)
end
end
@@ -109,7 +134,25 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'does not merge request' do
expect(MergeWorker).not_to receive(:perform_async)
- service.trigger(unrelated_pipeline)
+ service.process(mr_merge_if_green_enabled)
+ end
+ end
+
+ context 'when pipeline is merge request pipeline' do
+ let(:pipeline) do
+ create(:ci_pipeline, :success,
+ source: :merge_request_event,
+ ref: mr_merge_if_green_enabled.merge_ref_path,
+ merge_request: mr_merge_if_green_enabled,
+ merge_requests_as_head_pipeline: [mr_merge_if_green_enabled])
+ end
+
+ it 'merges the associated merge request' do
+ allow(mr_merge_if_green_enabled)
+ .to receive_messages(head_pipeline: pipeline, actual_head_pipeline: pipeline)
+
+ expect(MergeWorker).to receive(:perform_async)
+ service.process(mr_merge_if_green_enabled)
end
end
end
diff --git a/spec/services/auto_merge_service_spec.rb b/spec/services/auto_merge_service_spec.rb
new file mode 100644
index 00000000000..d0eefed3150
--- /dev/null
+++ b/spec/services/auto_merge_service_spec.rb
@@ -0,0 +1,140 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AutoMergeService do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '.all_strategies' do
+ subject { described_class.all_strategies }
+
+ it 'returns all strategies' do
+ is_expected.to eq(AutoMergeService::STRATEGIES)
+ end
+ end
+
+ describe '#available_strategies' do
+ subject { service.available_strategies(merge_request) }
+
+ let(:merge_request) { create(:merge_request) }
+ let(:pipeline_status) { :running }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: merge_request.source_project)
+
+ merge_request.update_head_pipeline
+ end
+
+ it 'returns available strategies' do
+ is_expected.to include('merge_when_pipeline_succeeds')
+ end
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it 'returns available strategies' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '.get_service_class' do
+ subject { described_class.get_service_class(strategy) }
+
+ let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
+
+ it 'returns service instance' do
+ is_expected.to eq(AutoMerge::MergeWhenPipelineSucceedsService)
+ end
+
+ context 'when strategy is not present' do
+ let(:strategy) { }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#execute' do
+ subject { service.execute(merge_request, strategy) }
+
+ let(:merge_request) { create(:merge_request) }
+ let(:pipeline_status) { :running }
+ let(:strategy) { AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS }
+
+ before do
+ create(:ci_pipeline, pipeline_status, ref: merge_request.source_branch,
+ sha: merge_request.diff_head_sha,
+ project: merge_request.source_project)
+
+ merge_request.update_head_pipeline
+ end
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:execute).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when the head piipeline succeeded' do
+ let(:pipeline_status) { :success }
+
+ it 'returns failed' do
+ is_expected.to eq(:failed)
+ end
+ end
+ end
+
+ describe '#process' do
+ subject { service.process(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:process).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#cancel' do
+ subject { service.cancel(merge_request) }
+
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'delegates to a relevant service instance' do
+ expect_next_instance_of(AutoMerge::MergeWhenPipelineSucceedsService) do |service|
+ expect(service).to receive(:cancel).with(merge_request)
+ end
+
+ subject
+ end
+
+ context 'when auto merge is not enabled' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns error' do
+ expect(subject[:message]).to eq("Can't cancel the automatic merge")
+ expect(subject[:status]).to eq(:error)
+ expect(subject[:http_status]).to eq(406)
+ end
+ end
+ end
+end
diff --git a/spec/services/base_count_service_spec.rb b/spec/services/base_count_service_spec.rb
index 090b2dcdd43..275bec9982d 100644
--- a/spec/services/base_count_service_spec.rb
+++ b/spec/services/base_count_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BaseCountService, :use_clean_rails_memory_store_caching do
diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb
index a715261cd6c..7d4fb04c6c0 100644
--- a/spec/services/boards/create_service_spec.rb
+++ b/spec/services/boards/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::CreateService do
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
index f0179e35652..33637419f83 100644
--- a/spec/services/boards/issues/create_service_spec.rb
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::Issues::CreateService do
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index aaad29536af..40878e24cb4 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::Issues::ListService do
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 6020f0771e5..16e2a2fba6b 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::Issues::MoveService do
diff --git a/spec/services/boards/list_service_spec.rb b/spec/services/boards/list_service_spec.rb
index 7518e9e9b75..c9d372ea166 100644
--- a/spec/services/boards/list_service_spec.rb
+++ b/spec/services/boards/list_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::ListService do
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index 7d3f5f86deb..295ec2c8156 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::Lists::CreateService do
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index 3c4eb6b3fc5..b936ef3837f 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::Lists::DestroyService do
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
index 82dbd1ee744..77b42392470 100644
--- a/spec/services/boards/lists/generate_service_spec.rb
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::Lists::GenerateService do
diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb
index 24e04eed642..2ebfd295fa2 100644
--- a/spec/services/boards/lists/list_service_spec.rb
+++ b/spec/services/boards/lists/list_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::Lists::ListService do
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 16dfb2ae6af..f8fc70ef2d6 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Boards::Lists::MoveService do
diff --git a/spec/services/boards/visits/latest_service_spec.rb b/spec/services/boards/visits/latest_service_spec.rb
index e55d599e2cc..c8a0a5e4243 100644
--- a/spec/services/boards/visits/latest_service_spec.rb
+++ b/spec/services/boards/visits/latest_service_spec.rb
@@ -23,6 +23,12 @@ describe Boards::Visits::LatestService do
service.execute
end
+
+ it 'queries for last N visits' do
+ expect(BoardProjectRecentVisit).to receive(:latest).with(user, project, count: 5).once
+
+ described_class.new(project_board.parent, user, count: 5).execute
+ end
end
context 'when a group board' do
@@ -42,6 +48,12 @@ describe Boards::Visits::LatestService do
service.execute
end
+
+ it 'queries for last N visits' do
+ expect(BoardGroupRecentVisit).to receive(:latest).with(user, group, count: 5).once
+
+ described_class.new(group_board.parent, user, count: 5).execute
+ end
end
end
end
diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb
index d88b2504133..41cbac4e8e9 100644
--- a/spec/services/chat_names/authorize_user_service_spec.rb
+++ b/spec/services/chat_names/authorize_user_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatNames::AuthorizeUserService do
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
index 5734b10109a..9d26f98cd56 100644
--- a/spec/services/chat_names/find_user_service_spec.rb
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do
diff --git a/spec/services/ci/archive_trace_service_spec.rb b/spec/services/ci/archive_trace_service_spec.rb
index 8e9cb65f3bc..44a77c29086 100644
--- a/spec/services/ci/archive_trace_service_spec.rb
+++ b/spec/services/ci/archive_trace_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::ArchiveTraceService, '#execute' do
diff --git a/spec/services/ci/compare_test_reports_service_spec.rb b/spec/services/ci/compare_test_reports_service_spec.rb
index a26c970a8f0..f5edd3a552d 100644
--- a/spec/services/ci/compare_test_reports_service_spec.rb
+++ b/spec/services/ci/compare_test_reports_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::CompareTestReportsService do
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 8497e90bd8b..867692d4d64 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::CreatePipelineService do
@@ -12,6 +14,7 @@ describe Ci::CreatePipelineService do
end
describe '#execute' do
+ # rubocop:disable Metrics/ParameterLists
def execute_service(
source: :push,
after: project.commit.id,
@@ -20,17 +23,23 @@ describe Ci::CreatePipelineService do
trigger_request: nil,
variables_attributes: nil,
merge_request: nil,
- push_options: nil)
+ push_options: nil,
+ source_sha: nil,
+ target_sha: nil,
+ save_on_errors: true)
params = { ref: ref,
before: '00000000',
after: after,
commits: [{ message: message }],
variables_attributes: variables_attributes,
- push_options: push_options }
+ push_options: push_options,
+ source_sha: source_sha,
+ target_sha: target_sha }
described_class.new(project, user, params).execute(
- source, trigger_request: trigger_request, merge_request: merge_request)
+ source, save_on_errors: save_on_errors, trigger_request: trigger_request, merge_request: merge_request)
end
+ # rubocop:enable Metrics/ParameterLists
context 'valid params' do
let(:pipeline) { execute_service }
@@ -49,6 +58,7 @@ describe Ci::CreatePipelineService do
expect(pipeline).to eq(project.ci_pipelines.last)
expect(pipeline).to have_attributes(user: user)
expect(pipeline).to have_attributes(status: 'pending')
+ expect(pipeline.iid).not_to be_nil
expect(pipeline.repository_source?).to be true
expect(pipeline.builds.first).to be_kind_of(Ci::Build)
end
@@ -300,6 +310,56 @@ describe Ci::CreatePipelineService do
it_behaves_like 'a failed pipeline'
end
+
+ context 'when config has ports' do
+ context 'in the main image' do
+ let(:ci_yaml) do
+ <<-EOS
+ image:
+ name: ruby:2.2
+ ports:
+ - 80
+ EOS
+ end
+
+ it_behaves_like 'a failed pipeline'
+ end
+
+ context 'in the job image' do
+ let(:ci_yaml) do
+ <<-EOS
+ image: ruby:2.2
+
+ test:
+ script: rspec
+ image:
+ name: ruby:2.2
+ ports:
+ - 80
+ EOS
+ end
+
+ it_behaves_like 'a failed pipeline'
+ end
+
+ context 'in the service' do
+ let(:ci_yaml) do
+ <<-EOS
+ image: ruby:2.2
+
+ test:
+ script: rspec
+ image: ruby:2.2
+ services:
+ - name: test
+ ports:
+ - 80
+ EOS
+ end
+
+ it_behaves_like 'a failed pipeline'
+ end
+ end
end
context 'when commit contains a [ci skip] directive' do
@@ -362,8 +422,7 @@ describe Ci::CreatePipelineService do
context 'when push options contain ci.skip' do
let(:push_options) do
- ['ci.skip',
- 'another push option']
+ { 'ci' => { 'skip' => true } }
end
it 'creates a pipline in the skipped state' do
@@ -389,6 +448,43 @@ describe Ci::CreatePipelineService do
expect(Ci::Build.all).to be_empty
expect(Ci::Pipeline.count).to eq(0)
end
+
+ describe '#iid' do
+ let(:internal_id) do
+ InternalId.find_by(project_id: project.id, usage: :ci_pipelines)
+ end
+
+ before do
+ expect_any_instance_of(Ci::Pipeline).to receive(:ensure_project_iid!)
+ .and_call_original
+ end
+
+ context 'when ci_pipeline_rewind_iid is enabled' do
+ before do
+ stub_feature_flags(ci_pipeline_rewind_iid: true)
+ end
+
+ it 'rewinds iid' do
+ result = execute_service
+
+ expect(result).not_to be_persisted
+ expect(internal_id.last_value).to eq(0)
+ end
+ end
+
+ context 'when ci_pipeline_rewind_iid is disabled' do
+ before do
+ stub_feature_flags(ci_pipeline_rewind_iid: false)
+ end
+
+ it 'does not rewind iid' do
+ result = execute_service
+
+ expect(result).not_to be_persisted
+ expect(internal_id.last_value).to eq(1)
+ end
+ end
+ end
end
context 'with manual actions' do
@@ -677,9 +773,13 @@ describe Ci::CreatePipelineService do
end
end
- describe 'Merge request pipelines' do
+ describe 'Pipelines for merge requests' do
let(:pipeline) do
- execute_service(source: source, merge_request: merge_request, ref: ref_name)
+ execute_service(source: source,
+ merge_request: merge_request,
+ ref: ref_name,
+ source_sha: source_sha,
+ target_sha: target_sha)
end
before do
@@ -687,9 +787,11 @@ describe Ci::CreatePipelineService do
end
let(:ref_name) { 'refs/heads/feature' }
+ let(:source_sha) { project.commit(ref_name).id }
+ let(:target_sha) { nil }
context 'when source is merge request' do
- let(:source) { :merge_request }
+ let(:source) { :merge_request_event }
context "when config has merge_requests keywords" do
let(:config) do
@@ -715,18 +817,43 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: Gitlab::Git.ref_name(ref_name),
+ source_branch: 'feature',
target_project: project,
target_branch: 'master')
end
- it 'creates a merge request pipeline' do
+ let(:ref_name) { merge_request.ref_path }
+
+ it 'creates a detached merge request pipeline' do
expect(pipeline).to be_persisted
- expect(pipeline).to be_merge_request
+ expect(pipeline).to be_merge_request_event
expect(pipeline.merge_request).to eq(merge_request)
expect(pipeline.builds.order(:stage_id).map(&:name)).to eq(%w[test])
end
+ it 'persists the specified source sha' do
+ expect(pipeline.source_sha).to eq(source_sha)
+ end
+
+ it 'does not persist target sha for detached merge request pipeline' do
+ expect(pipeline.target_sha).to be_nil
+ end
+
+ it 'schedules update for the head pipeline of the merge request' do
+ expect(UpdateHeadPipelineForMergeRequestWorker)
+ .to receive(:perform_async).with(merge_request.id)
+
+ pipeline
+ end
+
+ context 'when target sha is specified' do
+ let(:target_sha) { merge_request.target_branch_sha }
+
+ it 'persists the target sha' do
+ expect(pipeline.target_sha).to eq(target_sha)
+ end
+ end
+
context 'when ref is tag' do
let(:ref_name) { 'refs/tags/v1.1.0' }
@@ -740,15 +867,16 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: Gitlab::Git.ref_name(ref_name),
+ source_branch: 'feature',
target_project: target_project,
target_branch: 'master')
end
+ let(:ref_name) { 'refs/heads/feature' }
let!(:project) { fork_project(target_project, nil, repository: true) }
let!(:target_project) { create(:project, :repository) }
- it 'creates a merge request pipeline in the forked project' do
+ it 'creates a legacy detached merge request pipeline in the forked project' do
expect(pipeline).to be_persisted
expect(project.ci_pipelines).to eq([pipeline])
expect(target_project.ci_pipelines).to be_empty
@@ -766,7 +894,7 @@ describe Ci::CreatePipelineService do
}
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base]).to eq(["No stages / jobs for this pipeline."])
end
@@ -776,7 +904,7 @@ describe Ci::CreatePipelineService do
context 'when merge request is not specified' do
let(:merge_request) { nil }
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:merge_request]).to eq(["can't be blank"])
end
@@ -810,7 +938,7 @@ describe Ci::CreatePipelineService do
target_branch: 'master')
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
@@ -821,7 +949,7 @@ describe Ci::CreatePipelineService do
context 'when merge request is not specified' do
let(:merge_request) { nil }
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
@@ -845,12 +973,12 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
@@ -876,12 +1004,12 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
@@ -905,12 +1033,12 @@ describe Ci::CreatePipelineService do
let(:merge_request) do
create(:merge_request,
source_project: project,
- source_branch: ref_name,
+ source_branch: Gitlab::Git.ref_name(ref_name),
target_project: project,
target_branch: 'master')
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(pipeline).not_to be_persisted
expect(pipeline.errors[:base])
diff --git a/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb b/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb
index 80d82ba3ac9..fc5450ab33d 100644
--- a/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb
+++ b/spec/services/ci/destroy_expired_job_artifacts_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::DestroyExpiredJobArtifactsService, :clean_gitlab_redis_shared_state do
diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb
index d896f990470..bff2b3179fb 100644
--- a/spec/services/ci/destroy_pipeline_service_spec.rb
+++ b/spec/services/ci/destroy_pipeline_service_spec.rb
@@ -3,8 +3,8 @@
require 'spec_helper'
describe ::Ci::DestroyPipelineService do
- let(:project) { create(:project) }
- let!(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:project) { create(:project, :repository) }
+ let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.id) }
subject { described_class.new(project, user).execute(pipeline) }
@@ -17,6 +17,17 @@ describe ::Ci::DestroyPipelineService do
expect { pipeline.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
+ it 'clears the cache', :use_clean_rails_memory_store_caching do
+ create(:commit_status, :success, pipeline: pipeline, ref: pipeline.ref)
+
+ expect(project.pipeline_status.has_status?).to be_truthy
+
+ subject
+
+ # Need to use find to avoid memoization
+ expect(Project.find(project.id).pipeline_status.has_status?).to be_falsey
+ end
+
it 'does not log an audit event' do
expect { subject }.not_to change { SecurityEvent.count }
end
diff --git a/spec/services/ci/ensure_stage_service_spec.rb b/spec/services/ci/ensure_stage_service_spec.rb
index d17e30763d7..43bbd2130a4 100644
--- a/spec/services/ci/ensure_stage_service_spec.rb
+++ b/spec/services/ci/ensure_stage_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::EnsureStageService, '#execute' do
diff --git a/spec/services/ci/expire_pipeline_cache_service_spec.rb b/spec/services/ci/expire_pipeline_cache_service_spec.rb
new file mode 100644
index 00000000000..ff2d286465a
--- /dev/null
+++ b/spec/services/ci/expire_pipeline_cache_service_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::ExpirePipelineCacheService do
+ set(:user) { create(:user) }
+ set(:project) { create(:project) }
+ set(:pipeline) { create(:ci_pipeline, project: project) }
+ subject { described_class.new }
+
+ describe '#execute' do
+ it 'invalidates Etag caching for project pipelines path' do
+ pipelines_path = "/#{project.full_path}/pipelines.json"
+ new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json"
+ pipeline_path = "/#{project.full_path}/pipelines/#{pipeline.id}.json"
+
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path)
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(new_mr_pipelines_path)
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipeline_path)
+
+ subject.execute(pipeline)
+ end
+
+ it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do
+ pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master')
+ merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
+ merge_request_pipelines_path = "/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines.json"
+
+ allow_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch)
+ expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(merge_request_pipelines_path)
+
+ subject.execute(pipeline)
+ end
+
+ it 'updates the cached status for a project' do
+ expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline)
+ .with(pipeline)
+
+ subject.execute(pipeline)
+ end
+
+ context 'destroyed pipeline' do
+ let(:project_with_repo) { create(:project, :repository) }
+ let!(:pipeline_with_commit) { create(:ci_pipeline, :success, project: project_with_repo, sha: project_with_repo.commit.id) }
+
+ it 'clears the cache', :use_clean_rails_memory_store_caching do
+ create(:commit_status, :success, pipeline: pipeline_with_commit, ref: pipeline_with_commit.ref)
+
+ # Sanity check
+ expect(project_with_repo.pipeline_status.has_status?).to be_truthy
+
+ subject.execute(pipeline_with_commit, delete: true)
+
+ pipeline_with_commit.destroy!
+
+ # Need to use find to avoid memoization
+ expect(Project.find(project_with_repo.id).pipeline_status.has_status?).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/extract_sections_from_build_trace_service_spec.rb b/spec/services/ci/extract_sections_from_build_trace_service_spec.rb
index 28f2fa7903a..03c67c611fe 100644
--- a/spec/services/ci/extract_sections_from_build_trace_service_spec.rb
+++ b/spec/services/ci/extract_sections_from_build_trace_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::ExtractSectionsFromBuildTraceService, '#execute' do
diff --git a/spec/services/ci/pipeline_schedule_service_spec.rb b/spec/services/ci/pipeline_schedule_service_spec.rb
new file mode 100644
index 00000000000..f2ac53cb25a
--- /dev/null
+++ b/spec/services/ci/pipeline_schedule_service_spec.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::PipelineScheduleService do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:service) { described_class.new(project, user) }
+
+ describe '#execute' do
+ subject { service.execute(schedule) }
+
+ let(:schedule) { create(:ci_pipeline_schedule, project: project, owner: user) }
+
+ it 'schedules next run' do
+ expect(schedule).to receive(:schedule_next_run!)
+
+ subject
+ end
+
+ it 'runs RunPipelineScheduleWorker' do
+ expect(RunPipelineScheduleWorker)
+ .to receive(:perform_async).with(schedule.id, schedule.owner.id)
+
+ subject
+ end
+ end
+end
diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb
index f4ff818c479..76251b5b557 100644
--- a/spec/services/ci/pipeline_trigger_service_spec.rb
+++ b/spec/services/ci/pipeline_trigger_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::PipelineTriggerService do
diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb
index 330ec81e87d..634f865e2d8 100644
--- a/spec/services/ci/play_build_service_spec.rb
+++ b/spec/services/ci/play_build_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::PlayBuildService, '#execute' do
diff --git a/spec/services/ci/play_manual_stage_service_spec.rb b/spec/services/ci/play_manual_stage_service_spec.rb
new file mode 100644
index 00000000000..5d812745c7f
--- /dev/null
+++ b/spec/services/ci/play_manual_stage_service_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::PlayManualStageService, '#execute' do
+ let(:current_user) { create(:user) }
+ let(:pipeline) { create(:ci_pipeline, user: current_user) }
+ let(:project) { pipeline.project }
+ let(:service) { described_class.new(project, current_user, pipeline: pipeline) }
+ let(:stage_status) { 'manual' }
+
+ let(:stage) do
+ create(:ci_stage_entity,
+ pipeline: pipeline,
+ project: project,
+ name: 'test')
+ end
+
+ before do
+ project.add_maintainer(current_user)
+ create_builds_for_stage(status: stage_status)
+ end
+
+ context 'when pipeline has manual builds' do
+ before do
+ service.execute(stage)
+ end
+
+ it 'starts manual builds from pipeline' do
+ expect(pipeline.builds.manual.count).to eq(0)
+ end
+
+ it 'updates manual builds' do
+ pipeline.builds.each do |build|
+ expect(build.user).to eq(current_user)
+ end
+ end
+ end
+
+ context 'when pipeline has no manual builds' do
+ let(:stage_status) { 'failed' }
+
+ before do
+ service.execute(stage)
+ end
+
+ it 'does not update the builds' do
+ expect(pipeline.builds.failed.count).to eq(3)
+ end
+ end
+
+ context 'when user does not have permission on a specific build' do
+ before do
+ allow_any_instance_of(Ci::Build).to receive(:play)
+ .and_raise(Gitlab::Access::AccessDeniedError)
+
+ service.execute(stage)
+ end
+
+ it 'logs the error' do
+ expect(Gitlab::AppLogger).to receive(:error)
+ .exactly(stage.builds.manual.count)
+
+ service.execute(stage)
+ end
+ end
+
+ def create_builds_for_stage(options)
+ options.merge!({
+ when: 'manual',
+ pipeline: pipeline,
+ stage: stage.name,
+ stage_id: stage.id,
+ user: pipeline.user
+ })
+
+ create_list(:ci_build, 3, options)
+ end
+end
diff --git a/spec/services/ci/prepare_build_service_spec.rb b/spec/services/ci/prepare_build_service_spec.rb
new file mode 100644
index 00000000000..2d027f13e52
--- /dev/null
+++ b/spec/services/ci/prepare_build_service_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::PrepareBuildService do
+ describe '#execute' do
+ let(:build) { create(:ci_build, :preparing) }
+
+ subject { described_class.new(build).execute }
+
+ before do
+ allow(build).to receive(:prerequisites).and_return(prerequisites)
+ end
+
+ shared_examples 'build enqueueing' do
+ it 'enqueues the build' do
+ expect(build).to receive(:enqueue).once
+
+ subject
+ end
+ end
+
+ context 'build has unmet prerequisites' do
+ let(:prerequisite) { double(complete!: true) }
+ let(:prerequisites) { [prerequisite] }
+
+ it 'completes each prerequisite' do
+ expect(prerequisites).to all(receive(:complete!))
+
+ subject
+ end
+
+ include_examples 'build enqueueing'
+
+ context 'prerequisites fail to complete' do
+ before do
+ allow(build).to receive(:enqueue).and_return(false)
+ end
+
+ it 'drops the build' do
+ expect(build).to receive(:drop).with(:unmet_prerequisites).once
+
+ subject
+ end
+ end
+
+ context 'prerequisites raise an error' do
+ before do
+ allow(prerequisite).to receive(:complete!).and_raise Kubeclient::HttpError.new(401, 'unauthorized', nil)
+ end
+
+ it 'drops the build and notifies Sentry' do
+ expect(build).to receive(:drop).with(:unmet_prerequisites).once
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception)
+ .with(instance_of(Kubeclient::HttpError), hash_including(extra: { build_id: build.id }))
+
+ subject
+ end
+ end
+ end
+
+ context 'build has no prerequisites' do
+ let(:prerequisites) { [] }
+
+ include_examples 'build enqueueing'
+ end
+ end
+end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 6674d89518e..cadb519ccee 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::ProcessPipelineService, '#execute' do
diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb
index 20181387612..874945c8585 100644
--- a/spec/services/ci/register_job_service_spec.rb
+++ b/spec/services/ci/register_job_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
module Ci
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 87185891470..e9a26400723 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::RetryBuildService do
@@ -21,22 +23,23 @@ describe Ci::RetryBuildService do
REJECT_ACCESSORS =
%i[id status user token token_encrypted coverage trace runner
- artifacts_expire_at artifacts_file artifacts_metadata artifacts_size
+ artifacts_expire_at
created_at updated_at started_at finished_at queued_at erased_by
erased_at auto_canceled_by job_artifacts job_artifacts_archive
job_artifacts_metadata job_artifacts_trace job_artifacts_junit
job_artifacts_sast job_artifacts_dependency_scanning
job_artifacts_container_scanning job_artifacts_dast
job_artifacts_license_management job_artifacts_performance
- job_artifacts_codequality scheduled_at].freeze
+ job_artifacts_codequality job_artifacts_metrics scheduled_at].freeze
IGNORE_ACCESSORS =
%i[type lock_version target_url base_tags trace_sections
commit_id deployment erased_by_id project_id
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
- artifacts_file_store artifacts_metadata_store
- metadata runner_session trace_chunks].freeze
+ sourced_pipelines artifacts_file_store artifacts_metadata_store
+ metadata runner_session trace_chunks
+ artifacts_file artifacts_metadata artifacts_size].freeze
shared_examples 'build duplication' do
let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
@@ -95,7 +98,8 @@ describe Ci::RetryBuildService do
end
it 'has correct number of known attributes' do
- known_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS + IGNORE_ACCESSORS
+ processed_accessors = CLONE_ACCESSORS + REJECT_ACCESSORS
+ known_accessors = processed_accessors + IGNORE_ACCESSORS
# :tag_list is a special case, this accessor does not exist
# in reflected associations, comes from `act_as_taggable` and
@@ -108,7 +112,8 @@ describe Ci::RetryBuildService do
current_accessors.uniq!
- expect(known_accessors).to contain_exactly(*current_accessors)
+ expect(current_accessors).to include(*processed_accessors)
+ expect(known_accessors).to include(*current_accessors)
end
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 75042b29bea..e42de3cd48f 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::RetryPipelineService, '#execute' do
diff --git a/spec/services/ci/run_scheduled_build_service_spec.rb b/spec/services/ci/run_scheduled_build_service_spec.rb
index be2aad33ef4..ab8d9f4ba2e 100644
--- a/spec/services/ci/run_scheduled_build_service_spec.rb
+++ b/spec/services/ci/run_scheduled_build_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::RunScheduledBuildService do
diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb
index cdd3d851f61..890fa5bc009 100644
--- a/spec/services/ci/stop_environments_service_spec.rb
+++ b/spec/services/ci/stop_environments_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::StopEnvironmentsService do
@@ -103,6 +105,82 @@ describe Ci::StopEnvironmentsService do
end
end
+ describe '#execute_for_merge_request' do
+ subject { service.execute_for_merge_request(merge_request) }
+
+ let(:merge_request) { create(:merge_request, source_branch: 'feature', target_branch: 'master') }
+ let(:project) { merge_request.project }
+ let(:user) { create(:user) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ source: :merge_request_event,
+ merge_request: merge_request,
+ project: project,
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let!(:review_job) { create(:ci_build, :start_review_app, pipeline: pipeline, project: project) }
+ let!(:stop_review_job) { create(:ci_build, :stop_review_app, :manual, pipeline: pipeline, project: project) }
+
+ before do
+ review_job.deployment.success!
+ end
+
+ it 'has active environment at first' do
+ expect(pipeline.environments.first).to be_available
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'stops the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_stopped
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'does not stop the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_available
+ end
+ end
+
+ context 'when pipeline is not associated with environments' do
+ let!(:job) { create(:ci_build, pipeline: pipeline, project: project) }
+
+ it 'does not raise exception' do
+ expect { subject }.not_to raise_exception
+ end
+ end
+
+ context 'when pipeline is not a pipeline for merge request' do
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: 'feature',
+ sha: merge_request.diff_head_sha,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ it 'does not stop the active environment' do
+ subject
+
+ expect(pipeline.environments.first).to be_available
+ end
+ end
+ end
+
def expect_environment_stopped_on(branch)
expect_any_instance_of(Environment)
.to receive(:stop!)
diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb
index ca0c6be5da6..4b869385128 100644
--- a/spec/services/ci/update_build_queue_service_spec.rb
+++ b/spec/services/ci/update_build_queue_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::UpdateBuildQueueService do
diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb
index 7cc04c92d27..2b07dad7248 100644
--- a/spec/services/ci/update_runner_service_spec.rb
+++ b/spec/services/ci/update_runner_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::UpdateRunnerService do
diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
index f3036fbcb0e..84bca76e69b 100644
--- a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
+++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Applications::CheckIngressIpAddressService do
@@ -6,9 +8,17 @@ describe Clusters::Applications::CheckIngressIpAddressService do
let(:application) { create(:clusters_applications_ingress, :installed) }
let(:service) { described_class.new(application) }
let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
- let(:ingress) { [{ ip: '111.222.111.222' }] }
let(:lease_key) { "check_ingress_ip_address_service:#{application.id}" }
+ let(:ingress) do
+ [
+ {
+ ip: '111.222.111.222',
+ hostname: 'localhost.localdomain'
+ }
+ ]
+ end
+
let(:kube_service) do
::Kubeclient::Resource.new(
{
diff --git a/spec/services/clusters/applications/check_installation_progress_service_spec.rb b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
index 19446ce1cf8..a54bd85a11a 100644
--- a/spec/services/clusters/applications/check_installation_progress_service_spec.rb
+++ b/spec/services/clusters/applications/check_installation_progress_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
@@ -16,7 +18,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context "when phase is #{a_phase}" do
- context 'when not timeouted' do
+ context 'when not timed_out' do
it 'reschedule a new check' do
expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
expect(service).not_to receive(:remove_installation_pod)
@@ -33,14 +35,22 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
end
- shared_examples 'error logging' do
+ shared_examples 'error handling' do
context 'when installation raises a Kubeclient::HttpError' do
let(:cluster) { create(:cluster, :provided_by_user, :project) }
+ let(:logger) { service.send(:logger) }
+ let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
before do
application.update!(cluster: cluster)
- expect(service).to receive(:installation_phase).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil))
+ expect(service).to receive(:installation_phase).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'Unauthorized' }
+ let(:error_code) { 401 }
end
it 'shows the response code from the error' do
@@ -49,12 +59,6 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
expect(application).to be_errored.or(be_update_errored)
expect(application.status_reason).to eq('Kubernetes error: 401')
end
-
- it 'should log error' do
- expect(service.send(:logger)).to receive(:error)
-
- service.execute
- end
end
end
@@ -66,7 +70,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
context 'when application is updating' do
let(:application) { create(:clusters_applications_helm, :updating) }
- include_examples 'error logging'
+ include_examples 'error handling'
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
@@ -109,7 +113,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timeouted, :updating) }
+ let(:application) { create(:clusters_applications_helm, :timed_out, :updating) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
@@ -127,7 +131,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context 'when application is installing' do
- include_examples 'error logging'
+ include_examples 'error handling'
RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
@@ -170,7 +174,7 @@ describe Clusters::Applications::CheckInstallationProgressService, '#execute' do
end
context 'when timed out' do
- let(:application) { create(:clusters_applications_helm, :timeouted) }
+ let(:application) { create(:clusters_applications_helm, :timed_out) }
before do
expect(service).to receive(:installation_phase).once.and_return(phase)
diff --git a/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
new file mode 100644
index 00000000000..9ab83d913f5
--- /dev/null
+++ b/spec/services/clusters/applications/check_uninstall_progress_service_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::CheckUninstallProgressService do
+ RESCHEDULE_PHASES = Gitlab::Kubernetes::Pod::PHASES - [Gitlab::Kubernetes::Pod::SUCCEEDED, Gitlab::Kubernetes::Pod::FAILED].freeze
+
+ let(:application) { create(:clusters_applications_prometheus, :uninstalling) }
+ let(:service) { described_class.new(application) }
+ let(:phase) { Gitlab::Kubernetes::Pod::UNKNOWN }
+ let(:errors) { nil }
+ let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
+
+ before do
+ allow(service).to receive(:installation_errors).and_return(errors)
+ allow(service).to receive(:remove_installation_pod)
+ end
+
+ shared_examples 'a not yet terminated installation' do |a_phase|
+ let(:phase) { a_phase }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ context "when phase is #{a_phase}" do
+ context 'when not timed_out' do
+ it 'reschedule a new check' do
+ expect(worker_class).to receive(:perform_in).once
+ expect(service).not_to receive(:remove_installation_pod)
+
+ expect do
+ service.execute
+
+ application.reload
+ end.not_to change(application, :status)
+
+ expect(application.status_reason).to be_nil
+ end
+ end
+ end
+ end
+
+ context 'when application is installing' do
+ RESCHEDULE_PHASES.each { |phase| it_behaves_like 'a not yet terminated installation', phase }
+
+ context 'when installation POD succeeded' do
+ let(:phase) { Gitlab::Kubernetes::Pod::SUCCEEDED }
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'removes the installation POD' do
+ expect(service).to receive(:remove_installation_pod).once
+
+ service.execute
+ end
+
+ it 'destroys the application' do
+ expect(worker_class).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_destroyed
+ end
+
+ context 'an error occurs while destroying' do
+ before do
+ expect(application).to receive(:destroy!).once.and_raise("destroy failed")
+ end
+
+ it 'still removes the installation POD' do
+ expect(service).to receive(:remove_installation_pod).once
+
+ service.execute
+ end
+
+ it 'makes the application uninstall_errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Application uninstalled but failed to destroy: destroy failed')
+ end
+ end
+ end
+
+ context 'when installation POD failed' do
+ let(:phase) { Gitlab::Kubernetes::Pod::FAILED }
+ let(:errors) { 'test installation failed' }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Operation failed. Check pod logs for uninstall-prometheus for more details.')
+ end
+ end
+
+ context 'when timed out' do
+ let(:application) { create(:clusters_applications_prometheus, :timed_out, :uninstalling) }
+
+ before do
+ expect(service).to receive(:installation_phase).once.and_return(phase)
+ end
+
+ it 'make the application errored' do
+ expect(worker_class).not_to receive(:perform_in)
+
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Operation timed out. Check pod logs for uninstall-prometheus for more details.')
+ end
+ end
+
+ context 'when installation raises a Kubeclient::HttpError' do
+ let(:cluster) { create(:cluster, :provided_by_user, :project) }
+ let(:logger) { service.send(:logger) }
+ let(:error) { Kubeclient::HttpError.new(401, 'Unauthorized', nil) }
+
+ before do
+ application.update!(cluster: cluster)
+
+ expect(service).to receive(:installation_phase).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'Unauthorized' }
+ let(:error_code) { 401 }
+ end
+
+ it 'shows the response code from the error' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Kubernetes error: 401')
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/create_service_spec.rb b/spec/services/clusters/applications/create_service_spec.rb
index 3f621ed5944..bb86a742f0e 100644
--- a/spec/services/clusters/applications/create_service_spec.rb
+++ b/spec/services/clusters/applications/create_service_spec.rb
@@ -26,12 +26,6 @@ describe Clusters::Applications::CreateService do
end.to change(cluster, :application_helm)
end
- it 'schedules an install via worker' do
- expect(ClusterInstallAppWorker).to receive(:perform_async).with('helm', anything).once
-
- subject
- end
-
context 'application already installed' do
let!(:application) { create(:clusters_applications_helm, :installed, cluster: cluster) }
@@ -42,88 +36,101 @@ describe Clusters::Applications::CreateService do
end
it 'schedules an upgrade for the application' do
- expect(Clusters::Applications::ScheduleInstallationService).to receive(:new).with(application).and_call_original
+ expect(ClusterUpgradeAppWorker).to receive(:perform_async)
subject
end
end
- context 'cert manager application' do
- let(:params) do
- {
- application: 'cert_manager',
- email: 'test@example.com'
- }
- end
-
+ context 'known applications' do
before do
- allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ create(:clusters_applications_helm, :installed, cluster: cluster)
end
- it 'creates the application' do
- expect do
- subject
+ context 'cert manager application' do
+ let(:params) do
+ {
+ application: 'cert_manager',
+ email: 'test@example.com'
+ }
+ end
- cluster.reload
- end.to change(cluster, :application_cert_manager)
- end
+ before do
+ expect_any_instance_of(Clusters::Applications::CertManager)
+ .to receive(:make_scheduled!)
+ .and_call_original
+ end
- it 'sets the email' do
- expect(subject.email).to eq('test@example.com')
- end
- end
+ it 'creates the application' do
+ expect do
+ subject
- context 'jupyter application' do
- let(:params) do
- {
- application: 'jupyter',
- hostname: 'example.com'
- }
- end
+ cluster.reload
+ end.to change(cluster, :application_cert_manager)
+ end
- before do
- allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ it 'sets the email' do
+ expect(subject.email).to eq('test@example.com')
+ end
end
- it 'creates the application' do
- expect do
- subject
+ context 'jupyter application' do
+ let(:params) do
+ {
+ application: 'jupyter',
+ hostname: 'example.com'
+ }
+ end
- cluster.reload
- end.to change(cluster, :application_jupyter)
- end
+ before do
+ create(:clusters_applications_ingress, :installed, external_ip: "127.0.0.0", cluster: cluster)
+ expect_any_instance_of(Clusters::Applications::Jupyter)
+ .to receive(:make_scheduled!)
+ .and_call_original
+ end
- it 'sets the hostname' do
- expect(subject.hostname).to eq('example.com')
- end
+ it 'creates the application' do
+ expect do
+ subject
- it 'sets the oauth_application' do
- expect(subject.oauth_application).to be_present
- end
- end
+ cluster.reload
+ end.to change(cluster, :application_jupyter)
+ end
- context 'knative application' do
- let(:params) do
- {
- application: 'knative',
- hostname: 'example.com'
- }
- end
+ it 'sets the hostname' do
+ expect(subject.hostname).to eq('example.com')
+ end
- before do
- allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
+ it 'sets the oauth_application' do
+ expect(subject.oauth_application).to be_present
+ end
end
- it 'creates the application' do
- expect do
- subject
+ context 'knative application' do
+ let(:params) do
+ {
+ application: 'knative',
+ hostname: 'example.com'
+ }
+ end
- cluster.reload
- end.to change(cluster, :application_knative)
- end
+ before do
+ expect_any_instance_of(Clusters::Applications::Knative)
+ .to receive(:make_scheduled!)
+ .and_call_original
+ end
- it 'sets the hostname' do
- expect(subject.hostname).to eq('example.com')
+ it 'creates the application' do
+ expect do
+ subject
+
+ cluster.reload
+ end.to change(cluster, :application_knative)
+ end
+
+ it 'sets the hostname' do
+ expect(subject.hostname).to eq('example.com')
+ end
end
end
@@ -140,19 +147,21 @@ describe Clusters::Applications::CreateService do
using RSpec::Parameterized::TableSyntax
- before do
- allow_any_instance_of(Clusters::Applications::ScheduleInstallationService).to receive(:execute)
- end
-
- where(:application, :association, :allowed) do
- 'helm' | :application_helm | true
- 'ingress' | :application_ingress | true
- 'runner' | :application_runner | false
- 'jupyter' | :application_jupyter | false
- 'prometheus' | :application_prometheus | false
+ where(:application, :association, :allowed, :pre_create_helm) do
+ 'helm' | :application_helm | true | false
+ 'ingress' | :application_ingress | true | true
+ 'runner' | :application_runner | true | true
+ 'prometheus' | :application_prometheus | true | true
+ 'jupyter' | :application_jupyter | false | true
end
with_them do
+ before do
+ klass = "Clusters::Applications::#{application.titleize}"
+ allow_any_instance_of(klass.constantize).to receive(:make_scheduled!).and_call_original
+ create(:clusters_applications_helm, :installed, cluster: cluster) if pre_create_helm
+ end
+
let(:params) { { application: application } }
it 'executes for each application' do
@@ -168,5 +177,68 @@ describe Clusters::Applications::CreateService do
end
end
end
+
+ context 'when application is installable' do
+ shared_examples 'installable applications' do
+ it 'makes the application scheduled' do
+ expect do
+ subject
+ end.to change { Clusters::Applications::Helm.with_status(:scheduled).count }.by(1)
+ end
+
+ it 'schedules an install via worker' do
+ expect(ClusterInstallAppWorker)
+ .to receive(:perform_async)
+ .with(*worker_arguments)
+ .once
+
+ subject
+ end
+ end
+
+ context 'when application is associated with a cluster' do
+ let(:application) { create(:clusters_applications_helm, :installable, cluster: cluster) }
+ let(:worker_arguments) { [application.name, application.id] }
+
+ it_behaves_like 'installable applications'
+ end
+
+ context 'when application is not associated with a cluster' do
+ let(:worker_arguments) { [params[:application], kind_of(Numeric)] }
+
+ it_behaves_like 'installable applications'
+ end
+ end
+
+ context 'when installation is already in progress' do
+ let!(:application) { create(:clusters_applications_helm, :installing, cluster: cluster) }
+
+ it 'raises an exception' do
+ expect { subject }
+ .to raise_exception(StateMachines::InvalidTransition)
+ .and not_change(application.class.with_status(:scheduled), :count)
+ end
+
+ it 'does not schedule a cluster worker' do
+ expect(ClusterInstallAppWorker).not_to receive(:perform_async)
+ end
+ end
+
+ context 'when application is installed' do
+ %i(installed updated).each do |status|
+ let(:application) { create(:clusters_applications_helm, status, cluster: cluster) }
+
+ it 'schedules an upgrade via worker' do
+ expect(ClusterUpgradeAppWorker)
+ .to receive(:perform_async)
+ .with(application.name, application.id)
+ .once
+
+ subject
+
+ expect(application.reload).to be_scheduled
+ end
+ end
+ end
end
end
diff --git a/spec/services/clusters/applications/destroy_service_spec.rb b/spec/services/clusters/applications/destroy_service_spec.rb
new file mode 100644
index 00000000000..8d9dc6a0f11
--- /dev/null
+++ b/spec/services/clusters/applications/destroy_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::DestroyService, '#execute' do
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:user) { create(:user) }
+ let(:params) { { application: 'prometheus' } }
+ let(:service) { described_class.new(cluster, user, params) }
+ let(:test_request) { double }
+ let(:worker_class) { Clusters::Applications::UninstallWorker }
+
+ subject { service.execute(test_request) }
+
+ before do
+ allow(worker_class).to receive(:perform_async)
+ end
+
+ context 'application is not installed' do
+ it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
+ expect(worker_class).not_to receive(:perform_async)
+
+ expect { subject }
+ .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
+ .and not_change { Clusters::Applications::Prometheus.count }
+ .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count }
+ end
+ end
+
+ context 'application is installed' do
+ context 'application is schedulable' do
+ let!(:application) do
+ create(:clusters_applications_prometheus, :installed, cluster: cluster)
+ end
+
+ it 'makes application scheduled!' do
+ subject
+
+ expect(application.reload).to be_scheduled
+ end
+
+ it 'schedules UninstallWorker' do
+ expect(worker_class).to receive(:perform_async).with(application.name, application.id)
+
+ subject
+ end
+ end
+
+ context 'application is not schedulable' do
+ let!(:application) do
+ create(:clusters_applications_prometheus, :updating, cluster: cluster)
+ end
+
+ it 'raises StateMachines::InvalidTransition' do
+ expect(worker_class).not_to receive(:perform_async)
+
+ expect { subject }
+ .to raise_exception { StateMachines::InvalidTransition }
+ .and not_change { Clusters::Applications::Prometheus.with_status(:scheduled).count }
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/install_service_spec.rb b/spec/services/clusters/applications/install_service_spec.rb
index 018d9822d3e..9e1ae5e8742 100644
--- a/spec/services/clusters/applications/install_service_spec.rb
+++ b/spec/services/clusters/applications/install_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Applications::InstallService do
@@ -39,87 +41,39 @@ describe Clusters::Applications::InstallService do
expect(helm_client).to receive(:install).with(install_command).and_raise(error)
end
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'system failure' }
+ let(:error_code) { 500 }
+ end
+
it 'make the application errored' do
service.execute
expect(application).to be_errored
expect(application.status_reason).to match('Kubernetes error: 500')
end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::InstallService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::InstallService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- service.execute
- end
end
context 'a non kubernetes error happens' do
let(:application) { create(:clusters_applications_helm, :scheduled) }
- let(:error) { StandardError.new("something bad happened") }
+ let(:error) { StandardError.new('something bad happened') }
before do
- expect(application).to receive(:make_installing!).once.and_raise(error)
+ expect(helm_client).to receive(:install).with(install_command).and_raise(error)
end
- it 'make the application errored' do
- expect(helm_client).not_to receive(:install)
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'StandardError' }
+ let(:error_message) { 'something bad happened' }
+ let(:error_code) { nil }
+ end
+ it 'make the application errored' do
service.execute
expect(application).to be_errored
- expect(application.status_reason).to eq("Can't start installation process.")
- end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::InstallService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::InstallService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- service.execute
+ expect(application.status_reason).to eq('Failed to install.')
end
end
end
diff --git a/spec/services/clusters/applications/patch_service_spec.rb b/spec/services/clusters/applications/patch_service_spec.rb
new file mode 100644
index 00000000000..3ebe0540837
--- /dev/null
+++ b/spec/services/clusters/applications/patch_service_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::PatchService do
+ describe '#execute' do
+ let(:application) { create(:clusters_applications_knative, :scheduled) }
+ let!(:update_command) { application.update_command }
+ let(:service) { described_class.new(application) }
+ let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) }
+
+ before do
+ allow(service).to receive(:update_command).and_return(update_command)
+ allow(service).to receive(:helm_api).and_return(helm_client)
+ end
+
+ context 'when there are no errors' do
+ before do
+ expect(helm_client).to receive(:update).with(update_command)
+ allow(ClusterWaitForAppInstallationWorker).to receive(:perform_in).and_return(nil)
+ end
+
+ it 'make the application updating' do
+ expect(application.cluster).not_to be_nil
+ service.execute
+
+ expect(application).to be_updating
+ end
+
+ it 'schedule async installation status check' do
+ expect(ClusterWaitForAppInstallationWorker).to receive(:perform_in).once
+
+ service.execute
+ end
+ end
+
+ context 'when kubernetes cluster communication fails' do
+ let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
+
+ before do
+ expect(helm_client).to receive(:update).with(update_command).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'system failure' }
+ let(:error_code) { 500 }
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_update_errored
+ expect(application.status_reason).to match('Kubernetes error: 500')
+ end
+ end
+
+ context 'a non kubernetes error happens' do
+ let(:application) { create(:clusters_applications_knative, :scheduled) }
+ let(:error) { StandardError.new('something bad happened') }
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'StandardError' }
+ let(:error_message) { 'something bad happened' }
+ let(:error_code) { nil }
+ end
+
+ before do
+ expect(helm_client).to receive(:update).with(update_command).and_raise(error)
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_update_errored
+ expect(application.status_reason).to eq('Failed to update.')
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/schedule_installation_service_spec.rb b/spec/services/clusters/applications/schedule_installation_service_spec.rb
deleted file mode 100644
index 8380932dfaa..00000000000
--- a/spec/services/clusters/applications/schedule_installation_service_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe Clusters::Applications::ScheduleInstallationService do
- def count_scheduled
- application&.class&.with_status(:scheduled)&.count || 0
- end
-
- shared_examples 'a failing service' do
- it 'raise an exception' do
- expect(ClusterInstallAppWorker).not_to receive(:perform_async)
- count_before = count_scheduled
-
- expect { service.execute }.to raise_error(StandardError)
- expect(count_scheduled).to eq(count_before)
- end
- end
-
- describe '#execute' do
- let(:service) { described_class.new(application) }
-
- context 'when application is installable' do
- let(:application) { create(:clusters_applications_helm, :installable) }
-
- it 'make the application scheduled' do
- expect(ClusterInstallAppWorker).to receive(:perform_async).with(application.name, kind_of(Numeric)).once
-
- expect { service.execute }.to change { application.class.with_status(:scheduled).count }.by(1)
- end
- end
-
- context 'when installation is already in progress' do
- let(:application) { create(:clusters_applications_helm, :installing) }
-
- it_behaves_like 'a failing service'
- end
-
- context 'when application is nil' do
- let(:application) { nil }
-
- it_behaves_like 'a failing service'
- end
-
- context 'when application cannot be persisted' do
- let(:application) { create(:clusters_applications_helm) }
-
- before do
- expect(application).to receive(:make_scheduled!).once.and_raise(ActiveRecord::RecordInvalid)
- end
-
- it_behaves_like 'a failing service'
- end
-
- context 'when application is installed' do
- let(:application) { create(:clusters_applications_helm, :installed) }
-
- it 'schedules an upgrade via worker' do
- expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once
-
- service.execute
-
- expect(application).to be_scheduled
- end
- end
-
- context 'when application is updated' do
- let(:application) { create(:clusters_applications_helm, :updated) }
-
- it 'schedules an upgrade via worker' do
- expect(ClusterUpgradeAppWorker).to receive(:perform_async).with(application.name, application.id).once
-
- service.execute
-
- expect(application).to be_scheduled
- end
- end
- end
-end
diff --git a/spec/services/clusters/applications/uninstall_service_spec.rb b/spec/services/clusters/applications/uninstall_service_spec.rb
new file mode 100644
index 00000000000..16497d752b2
--- /dev/null
+++ b/spec/services/clusters/applications/uninstall_service_spec.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::UninstallService, '#execute' do
+ let(:application) { create(:clusters_applications_prometheus, :scheduled) }
+ let(:service) { described_class.new(application) }
+ let(:helm_client) { instance_double(Gitlab::Kubernetes::Helm::Api) }
+ let(:worker_class) { Clusters::Applications::WaitForUninstallAppWorker }
+
+ before do
+ allow(service).to receive(:helm_api).and_return(helm_client)
+ end
+
+ context 'when there are no errors' do
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand))
+ allow(worker_class).to receive(:perform_in).and_return(nil)
+ end
+
+ it 'make the application to be uninstalling' do
+ expect(application.cluster).not_to be_nil
+ service.execute
+
+ expect(application).to be_uninstalling
+ end
+
+ it 'schedule async installation status check' do
+ expect(worker_class).to receive(:perform_in).once
+
+ service.execute
+ end
+ end
+
+ context 'when k8s cluster communication fails' do
+ let(:error) { Kubeclient::HttpError.new(500, 'system failure', nil) }
+
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'system failure' }
+ let(:error_code) { 500 }
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to match('Kubernetes error: 500')
+ end
+ end
+
+ context 'a non kubernetes error happens' do
+ let(:application) { create(:clusters_applications_prometheus, :scheduled) }
+ let(:error) { StandardError.new('something bad happened') }
+
+ before do
+ expect(helm_client).to receive(:uninstall).with(kind_of(Gitlab::Kubernetes::Helm::DeleteCommand)).and_raise(error)
+ end
+
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'StandardError' }
+ let(:error_message) { 'something bad happened' }
+ let(:error_code) { nil }
+ end
+
+ it 'make the application errored' do
+ service.execute
+
+ expect(application).to be_uninstall_errored
+ expect(application.status_reason).to eq('Failed to uninstall.')
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/update_service_spec.rb b/spec/services/clusters/applications/update_service_spec.rb
new file mode 100644
index 00000000000..2d299882af0
--- /dev/null
+++ b/spec/services/clusters/applications/update_service_spec.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::UpdateService do
+ include TestRequestHelpers
+
+ let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:user) { create(:user) }
+ let(:params) { { application: 'knative', hostname: 'udpate.example.com' } }
+ let(:service) { described_class.new(cluster, user, params) }
+
+ subject { service.execute(test_request) }
+
+ describe '#execute' do
+ before do
+ allow(ClusterPatchAppWorker).to receive(:perform_async)
+ end
+
+ context 'application is not installed' do
+ it 'raises Clusters::Applications::BaseService::InvalidApplicationError' do
+ expect(ClusterPatchAppWorker).not_to receive(:perform_async)
+
+ expect { subject }
+ .to raise_exception { Clusters::Applications::BaseService::InvalidApplicationError }
+ .and not_change { Clusters::Applications::Knative.count }
+ .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
+ end
+ end
+
+ context 'application is installed' do
+ context 'application is schedulable' do
+ let!(:application) do
+ create(:clusters_applications_knative, status: 3, cluster: cluster)
+ end
+
+ it 'updates the application data' do
+ expect do
+ subject
+ end.to change { application.reload.hostname }.to(params[:hostname])
+ end
+
+ it 'makes application scheduled!' do
+ subject
+
+ expect(application.reload).to be_scheduled
+ end
+
+ it 'schedules ClusterPatchAppWorker' do
+ expect(ClusterPatchAppWorker).to receive(:perform_async)
+
+ subject
+ end
+ end
+
+ context 'application is not schedulable' do
+ let!(:application) do
+ create(:clusters_applications_knative, status: 4, cluster: cluster)
+ end
+
+ it 'raises StateMachines::InvalidTransition' do
+ expect(ClusterPatchAppWorker).not_to receive(:perform_async)
+
+ expect { subject }
+ .to raise_exception { StateMachines::InvalidTransition }
+ .and not_change { application.reload.hostname }
+ .and not_change { Clusters::Applications::Knative.with_status(:scheduled).count }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/clusters/applications/upgrade_service_spec.rb b/spec/services/clusters/applications/upgrade_service_spec.rb
index 1822fc38dbd..a80b1d9127c 100644
--- a/spec/services/clusters/applications/upgrade_service_spec.rb
+++ b/spec/services/clusters/applications/upgrade_service_spec.rb
@@ -41,41 +41,18 @@ describe Clusters::Applications::UpgradeService do
expect(helm_client).to receive(:update).with(install_command).and_raise(error)
end
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'Kubeclient::HttpError' }
+ let(:error_message) { 'system failure' }
+ let(:error_code) { 500 }
+ end
+
it 'make the application errored' do
service.execute
expect(application).to be_update_errored
expect(application.status_reason).to match('Kubernetes error: 500')
end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::UpgradeService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'Kubeclient::HttpError',
- message: 'system failure',
- service: 'Clusters::Applications::UpgradeService',
- app_id: application.id,
- project_ids: application.cluster.project_ids,
- group_ids: [],
- error_code: 500
- }
- )
-
- service.execute
- end
end
context 'a non kubernetes error happens' do
@@ -83,45 +60,20 @@ describe Clusters::Applications::UpgradeService do
let(:error) { StandardError.new('something bad happened') }
before do
- expect(application).to receive(:make_updating!).once.and_raise(error)
+ expect(helm_client).to receive(:update).with(install_command).and_raise(error)
end
- it 'make the application errored' do
- expect(helm_client).not_to receive(:update)
+ include_examples 'logs kubernetes errors' do
+ let(:error_name) { 'StandardError' }
+ let(:error_message) { 'something bad happened' }
+ let(:error_code) { nil }
+ end
+ it 'make the application errored' do
service.execute
expect(application).to be_update_errored
- expect(application.status_reason).to eq("Can't start upgrade process.")
- end
-
- it 'logs errors' do
- expect(service.send(:logger)).to receive(:error).with(
- {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::UpgradeService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
- error,
- extra: {
- exception: 'StandardError',
- error_code: nil,
- message: 'something bad happened',
- service: 'Clusters::Applications::UpgradeService',
- app_id: application.id,
- project_ids: application.cluster.projects.pluck(:id),
- group_ids: []
- }
- )
-
- service.execute
+ expect(application.status_reason).to eq('Failed to upgrade.')
end
end
end
diff --git a/spec/services/clusters/build_service_spec.rb b/spec/services/clusters/build_service_spec.rb
index da0cb42b3a1..f3e852726f4 100644
--- a/spec/services/clusters/build_service_spec.rb
+++ b/spec/services/clusters/build_service_spec.rb
@@ -21,5 +21,13 @@ describe Clusters::BuildService do
is_expected.to be_group_type
end
end
+
+ describe 'when cluster subject is an instance' do
+ let(:cluster_subject) { Clusters::Instance.new }
+
+ it 'sets the cluster_type to instance_type' do
+ is_expected.to be_instance_type
+ end
+ end
end
end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 274880f2c49..ecf0a9c9dce 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::CreateService do
diff --git a/spec/services/clusters/gcp/fetch_operation_service_spec.rb b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
index 55f123ee786..23da8004a7d 100644
--- a/spec/services/clusters/gcp/fetch_operation_service_spec.rb
+++ b/spec/services/clusters/gcp/fetch_operation_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Gcp::FetchOperationService do
diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
index 18f218fc236..be052a07da7 100644
--- a/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/create_or_update_namespace_service_spec.rb
@@ -113,7 +113,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService, '#execute' d
it 'does not create any Clusters::KubernetesNamespace' do
subject
- expect(cluster.kubernetes_namespace).to eq(kubernetes_namespace)
+ expect(cluster.kubernetes_namespaces).to eq([kubernetes_namespace])
end
it 'creates project service account' do
diff --git a/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb
index 11a65d0c300..382b9043566 100644
--- a/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb
+++ b/spec/services/clusters/gcp/kubernetes/create_or_update_service_account_service_spec.rb
@@ -89,7 +89,7 @@ describe Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService do
it_behaves_like 'creates service account and token'
- it 'should create a cluster role binding with cluster-admin access' do
+ it 'creates a cluster role binding with cluster-admin access' do
subject
expect(WebMock).to have_requested(:post, api_url + "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings").with(
diff --git a/spec/services/clusters/gcp/provision_service_spec.rb b/spec/services/clusters/gcp/provision_service_spec.rb
index c0bdac40938..dfd15690a1f 100644
--- a/spec/services/clusters/gcp/provision_service_spec.rb
+++ b/spec/services/clusters/gcp/provision_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Gcp::ProvisionService do
diff --git a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
index 2ee2fa51f63..9611b2080ba 100644
--- a/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
+++ b/spec/services/clusters/gcp/verify_provision_status_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::Gcp::VerifyProvisionStatusService do
diff --git a/spec/services/clusters/refresh_service_spec.rb b/spec/services/clusters/refresh_service_spec.rb
index 58ab3c3cf73..5bc8a709941 100644
--- a/spec/services/clusters/refresh_service_spec.rb
+++ b/spec/services/clusters/refresh_service_spec.rb
@@ -93,7 +93,7 @@ describe Clusters::RefreshService do
let(:group) { cluster.group }
let(:project) { create(:project, group: group) }
- include_examples 'creates a kubernetes namespace'
+ include_examples 'does not create a kubernetes namespace'
context 'when project already has kubernetes namespace' do
before do
@@ -103,5 +103,11 @@ describe Clusters::RefreshService do
include_examples 'does not create a kubernetes namespace'
end
end
+
+ context 'cluster is not managed' do
+ let!(:cluster) { create(:cluster, :project, :not_managed, projects: [project]) }
+
+ include_examples 'does not create a kubernetes namespace'
+ end
end
end
diff --git a/spec/services/clusters/update_service_spec.rb b/spec/services/clusters/update_service_spec.rb
index b2e6ebecd4a..21b37f88fd8 100644
--- a/spec/services/clusters/update_service_spec.rb
+++ b/spec/services/clusters/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Clusters::UpdateService do
diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb
index 77595d7ba2d..2c012f080dd 100644
--- a/spec/services/cohorts_service_spec.rb
+++ b/spec/services/cohorts_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CohortsService do
diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb
index 9e15eae8c13..fadd43635a6 100644
--- a/spec/services/compare_service_spec.rb
+++ b/spec/services/compare_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CompareService do
@@ -17,5 +19,18 @@ describe CompareService do
it { expect(subject.diffs.size).to eq(3) }
end
+
+ context 'compare with target branch that does not exist' do
+ subject { service.execute(project, 'non-existent-ref') }
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'compare with source branch that does not exist' do
+ let(:service) { described_class.new(project, 'non-existent-branch') }
+ subject { service.execute(project, 'non-existent-ref') }
+
+ it { expect(subject).to be_nil }
+ end
end
end
diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb
index 38096a080a7..0d34c7f9a82 100644
--- a/spec/services/create_branch_service_spec.rb
+++ b/spec/services/create_branch_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CreateBranchService do
diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
index b6ab6b8271c..f6b6989b955 100644
--- a/spec/services/create_snippet_service_spec.rb
+++ b/spec/services/create_snippet_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CreateSnippetService do
diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb
index 9c9fba030e7..b8064c2cbc1 100644
--- a/spec/services/delete_branch_service_spec.rb
+++ b/spec/services/delete_branch_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeleteBranchService do
@@ -6,20 +8,24 @@ describe DeleteBranchService do
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
+ shared_examples 'a deleted branch' do |branch_name|
+ it 'removes the branch' do
+ expect(branch_exists?(branch_name)).to be true
+
+ result = service.execute(branch_name)
+
+ expect(result.status).to eq :success
+ expect(branch_exists?(branch_name)).to be false
+ end
+ end
+
describe '#execute' do
context 'when user has access to push to repository' do
before do
project.add_developer(user)
end
- it 'removes the branch' do
- expect(branch_exists?('feature')).to be true
-
- result = service.execute('feature')
-
- expect(result[:status]).to eq :success
- expect(branch_exists?('feature')).to be false
- end
+ it_behaves_like 'a deleted branch', 'feature'
end
context 'when user does not have access to push to repository' do
@@ -28,8 +34,8 @@ describe DeleteBranchService do
result = service.execute('feature')
- expect(result[:status]).to eq :error
- expect(result[:message]).to eq 'You dont have push access to repo'
+ expect(result.status).to eq :error
+ expect(result.message).to eq 'You dont have push access to repo'
expect(branch_exists?('feature')).to be true
end
end
diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb
index 0de02576203..dffc2bd93ee 100644
--- a/spec/services/delete_merged_branches_service_spec.rb
+++ b/spec/services/delete_merged_branches_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeleteMergedBranchesService do
diff --git a/spec/services/deploy_keys/create_service_spec.rb b/spec/services/deploy_keys/create_service_spec.rb
index 7a604c0cadd..a55f1561194 100644
--- a/spec/services/deploy_keys/create_service_spec.rb
+++ b/spec/services/deploy_keys/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeployKeys::CreateService do
diff --git a/spec/services/deploy_tokens/create_service_spec.rb b/spec/services/deploy_tokens/create_service_spec.rb
index 3a2bbf1ecd1..886ffd4593d 100644
--- a/spec/services/deploy_tokens/create_service_spec.rb
+++ b/spec/services/deploy_tokens/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeployTokens::CreateService do
@@ -9,11 +11,11 @@ describe DeployTokens::CreateService do
subject { described_class.new(project, user, deploy_token_params).execute }
context 'when the deploy token is valid' do
- it 'should create a new DeployToken' do
+ it 'creates a new DeployToken' do
expect { subject }.to change { DeployToken.count }.by(1)
end
- it 'should create a new ProjectDeployToken' do
+ it 'creates a new ProjectDeployToken' do
expect { subject }.to change { ProjectDeployToken.count }.by(1)
end
@@ -25,7 +27,7 @@ describe DeployTokens::CreateService do
context 'when expires at date is not passed' do
let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') }
- it 'should set Forever.date' do
+ it 'sets Forever.date' do
expect(subject.read_attribute(:expires_at)).to eq(Forever.date)
end
end
@@ -33,11 +35,11 @@ describe DeployTokens::CreateService do
context 'when the deploy token is invalid' do
let(:deploy_token_params) { attributes_for(:deploy_token, read_repository: false, read_registry: false) }
- it 'should not create a new DeployToken' do
+ it 'does not create a new DeployToken' do
expect { subject }.not_to change { DeployToken.count }
end
- it 'should not create a new ProjectDeployToken' do
+ it 'does not create a new ProjectDeployToken' do
expect { subject }.not_to change { ProjectDeployToken.count }
end
end
diff --git a/spec/services/discussions/resolve_service_spec.rb b/spec/services/discussions/resolve_service_spec.rb
index 4e0d4749239..5b99430cb75 100644
--- a/spec/services/discussions/resolve_service_spec.rb
+++ b/spec/services/discussions/resolve_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Discussions::ResolveService do
diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb
index 2b84206318f..60ec83e9062 100644
--- a/spec/services/discussions/update_diff_position_service_spec.rb
+++ b/spec/services/discussions/update_diff_position_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Discussions::UpdateDiffPositionService do
diff --git a/spec/services/emails/confirm_service_spec.rb b/spec/services/emails/confirm_service_spec.rb
index 2b2c31e2521..6a274ca9dfe 100644
--- a/spec/services/emails/confirm_service_spec.rb
+++ b/spec/services/emails/confirm_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Emails::ConfirmService do
diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb
index 54692c88623..87f93ec97c9 100644
--- a/spec/services/emails/create_service_spec.rb
+++ b/spec/services/emails/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Emails::CreateService do
diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb
index c3204fac3df..5abe8da2529 100644
--- a/spec/services/emails/destroy_service_spec.rb
+++ b/spec/services/emails/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Emails::DestroyService do
diff --git a/spec/services/error_tracking/list_issues_service_spec.rb b/spec/services/error_tracking/list_issues_service_spec.rb
index 9d4fc62f923..3a8f3069911 100644
--- a/spec/services/error_tracking/list_issues_service_spec.rb
+++ b/spec/services/error_tracking/list_issues_service_spec.rb
@@ -53,7 +53,10 @@ describe ErrorTracking::ListIssuesService do
before do
allow(error_tracking_setting)
.to receive(:list_sentry_issues)
- .and_return(error: 'Sentry response status code: 401')
+ .and_return(
+ error: 'Sentry response status code: 401',
+ error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_NON_20X_RESPONSE
+ )
end
it 'returns the error' do
@@ -64,6 +67,25 @@ describe ErrorTracking::ListIssuesService do
)
end
end
+
+ context 'when list_sentry_issues returns error with http_status' do
+ before do
+ allow(error_tracking_setting)
+ .to receive(:list_sentry_issues)
+ .and_return(
+ error: 'Sentry API response is missing keys. key not found: "id"',
+ error_type: ErrorTracking::ProjectErrorTrackingSetting::SENTRY_API_ERROR_TYPE_MISSING_KEYS
+ )
+ end
+
+ it 'returns the error with correct http_status' do
+ expect(result).to eq(
+ status: :error,
+ http_status: :internal_server_error,
+ message: 'Sentry API response is missing keys. key not found: "id"'
+ )
+ end
+ end
end
context 'with unauthorized user' do
diff --git a/spec/services/error_tracking/list_projects_service_spec.rb b/spec/services/error_tracking/list_projects_service_spec.rb
index 9f25a633deb..730fccc599e 100644
--- a/spec/services/error_tracking/list_projects_service_spec.rb
+++ b/spec/services/error_tracking/list_projects_service_spec.rb
@@ -32,7 +32,7 @@ describe ErrorTracking::ListProjectsService do
end
context 'set model attributes to new values' do
- let(:new_api_url) { new_api_host + 'api/0/projects/' }
+ let(:new_api_url) { new_api_host + 'api/0/projects/org/proj/' }
before do
expect(error_tracking_setting).to receive(:list_sentry_projects)
@@ -51,14 +51,28 @@ describe ErrorTracking::ListProjectsService do
end
context 'sentry client raises exception' do
- before do
- expect(error_tracking_setting).to receive(:list_sentry_projects)
- .and_raise(Sentry::Client::Error, 'Sentry response status code: 500')
+ context 'Sentry::Client::Error' do
+ before do
+ expect(error_tracking_setting).to receive(:list_sentry_projects)
+ .and_raise(Sentry::Client::Error, 'Sentry response status code: 500')
+ end
+
+ it 'returns error response' do
+ expect(result[:message]).to eq('Sentry response status code: 500')
+ expect(result[:http_status]).to eq(:bad_request)
+ end
end
- it 'returns error response' do
- expect(result[:message]).to eq('Sentry response status code: 500')
- expect(result[:http_status]).to eq(:bad_request)
+ context 'Sentry::Client::MissingKeysError' do
+ before do
+ expect(error_tracking_setting).to receive(:list_sentry_projects)
+ .and_raise(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"')
+ end
+
+ it 'returns error response' do
+ expect(result[:message]).to eq('Sentry API response is missing keys. key not found: "id"')
+ expect(result[:http_status]).to eq(:internal_server_error)
+ end
end
end
@@ -121,7 +135,7 @@ describe ErrorTracking::ListProjectsService do
context 'error_tracking_setting is nil' do
let(:error_tracking_setting) { build(:project_error_tracking_setting) }
- let(:new_api_url) { new_api_host + 'api/0/projects/' }
+ let(:new_api_url) { new_api_host + 'api/0/projects/org/proj/' }
before do
expect(project).to receive(:build_error_tracking_setting).once
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 443665c9959..9f2c3fec62c 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EventCreateService do
diff --git a/spec/services/events/render_service_spec.rb b/spec/services/events/render_service_spec.rb
index 075cb45e46c..a623a05a56d 100644
--- a/spec/services/events/render_service_spec.rb
+++ b/spec/services/events/render_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Events::RenderService do
diff --git a/spec/services/files/create_service_spec.rb b/spec/services/files/create_service_spec.rb
index 751b7160276..195f56a2909 100644
--- a/spec/services/files/create_service_spec.rb
+++ b/spec/services/files/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe Files::CreateService do
diff --git a/spec/services/files/delete_service_spec.rb b/spec/services/files/delete_service_spec.rb
index 309802ce733..b849def06fc 100644
--- a/spec/services/files/delete_service_spec.rb
+++ b/spec/services/files/delete_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe Files::DeleteService do
diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb
index 84c48d63c64..0f51c72019e 100644
--- a/spec/services/files/multi_service_spec.rb
+++ b/spec/services/files/multi_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe Files::MultiService do
@@ -235,6 +237,22 @@ describe Files::MultiService do
expect(blob).to be_present
end
end
+
+ context 'when force is set to true and branch already exists' do
+ let(:commit_params) do
+ {
+ commit_message: commit_message,
+ branch_name: 'feature',
+ start_branch: 'master',
+ actions: actions,
+ force: true
+ }
+ end
+
+ it 'is still a success' do
+ expect(subject.execute[:status]).to eq(:success)
+ end
+ end
end
def update_file(path)
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index 23db35c2418..37869b176ef 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe Files::UpdateService do
diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb
new file mode 100644
index 00000000000..4a2ec769116
--- /dev/null
+++ b/spec/services/git/base_hooks_service_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::BaseHooksService do
+ include RepoHelpers
+ include GitHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:service) { described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
+
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { 'refs/tags/v1.1.0' }
+
+ describe 'with remote mirrors' do
+ class TestService < described_class
+ def commits
+ []
+ end
+ end
+
+ let(:project) { create(:project, :repository, :remote_mirror) }
+
+ subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
+
+ before do
+ expect(subject).to receive(:execute_project_hooks)
+ end
+
+ context 'when remote mirror feature is enabled' do
+ it 'fails stuck remote mirrors' do
+ allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'updates remote mirrors' do
+ expect(project).to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+
+ context 'when remote mirror feature is disabled' do
+ before do
+ stub_application_setting(mirror_available: false)
+ end
+
+ context 'with remote mirrors global setting overridden' do
+ before do
+ project.remote_mirror_available_overridden = true
+ end
+
+ it 'fails stuck remote mirrors' do
+ allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
+ expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'updates remote mirrors' do
+ expect(project).to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+
+ context 'without remote mirrors global setting overridden' do
+ before do
+ project.remote_mirror_available_overridden = false
+ end
+
+ it 'does not fails stuck remote mirrors' do
+ expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
+
+ subject.execute
+ end
+
+ it 'does not updates remote mirrors' do
+ expect(project).not_to receive(:update_remote_mirrors)
+
+ subject.execute
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb
new file mode 100644
index 00000000000..22faa996015
--- /dev/null
+++ b/spec/services/git/branch_hooks_service_spec.rb
@@ -0,0 +1,347 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::BranchHooksService do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { project.creator }
+
+ let(:branch) { project.default_branch }
+ let(:ref) { "refs/heads/#{branch}" }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:oldrev) { commit.parent_id }
+ let(:newrev) { commit.id }
+
+ let(:service) do
+ described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ end
+
+ it 'update remote mirrors' do
+ expect(service).to receive(:update_remote_mirrors).and_call_original
+
+ service.execute
+ end
+
+ describe "Git Push Data" do
+ subject(:push_data) { service.execute }
+
+ it 'has expected push data attributes' do
+ is_expected.to match a_hash_including(
+ object_kind: 'push',
+ before: oldrev,
+ after: newrev,
+ ref: ref,
+ user_id: user.id,
+ user_name: user.name,
+ project_id: project.id
+ )
+ end
+
+ context "with repository data" do
+ subject { push_data[:repository] }
+
+ it 'has expected attributes' do
+ is_expected.to match a_hash_including(
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url
+ )
+ end
+ end
+
+ context "with commits" do
+ subject { push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+
+ context "the commit" do
+ subject { push_data[:commits].first }
+
+ it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) }
+
+ it 'includes expected commit data' do
+ is_expected.to match a_hash_including(
+ id: commit.id,
+ message: commit.safe_message,
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ commit.id
+ ].join('/')
+ )
+ end
+
+ context "with a author" do
+ subject { push_data[:commits].first[:author] }
+
+ it 'includes expected author data' do
+ is_expected.to match a_hash_including(
+ name: commit.author_name,
+ email: commit.author_email
+ )
+ end
+ end
+ end
+ end
+ end
+
+ describe 'Push Event' do
+ let(:event) { Event.find_by_action(Event::PUSHED) }
+
+ before do
+ service.execute
+ end
+
+ context "with an existing branch" do
+ it 'generates a push event with one commit' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to eq(oldrev)
+ expect(event.push_event_payload.commit_to).to eq(newrev)
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to eq(1)
+ end
+ end
+
+ context "with a new branch" do
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'generates a push event with more than one commit' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to be_nil
+ expect(event.push_event_payload.commit_to).to eq(newrev)
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to be > 1
+ end
+ end
+
+ context 'removing a branch' do
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'generates a push event with no commits' do
+ expect(event).to be_an_instance_of(PushEvent)
+ expect(event.project).to eq(project)
+ expect(event.action).to eq(Event::PUSHED)
+ expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
+ expect(event.push_event_payload.commit_from).to eq(oldrev)
+ expect(event.push_event_payload.commit_to).to be_nil
+ expect(event.push_event_payload.ref).to eq('master')
+ expect(event.push_event_payload.commit_count).to eq(0)
+ end
+ end
+ end
+
+ describe 'Invalidating project cache' do
+ let(:commit_id) do
+ project.repository.update_file(
+ user, 'README.md', '', message: 'Update', branch_name: branch
+ )
+ end
+
+ let(:commit) { project.repository.commit(commit_id) }
+ let(:blank_sha) { Gitlab::Git::BLANK_SHA }
+
+ def clears_cache(extended: [])
+ expect(ProjectCacheWorker)
+ .to receive(:perform_async)
+ .with(project.id, extended, %i[commit_count repository_size])
+
+ service.execute
+ end
+
+ def clears_extended_cache
+ clears_cache(extended: %i[readme])
+ end
+
+ context 'on default branch' do
+ context 'create' do
+ # FIXME: When creating the default branch,the cache worker runs twice
+ before do
+ allow(ProjectCacheWorker).to receive(:perform_async)
+ end
+
+ let(:oldrev) { blank_sha }
+
+ it { clears_cache }
+ end
+
+ context 'update' do
+ it { clears_extended_cache }
+ end
+
+ context 'remove' do
+ let(:newrev) { blank_sha }
+
+ # TODO: this case should pass, but we only take account of added files
+ it { clears_cache }
+ end
+ end
+
+ context 'on ordinary branch' do
+ let(:branch) { 'fix' }
+
+ context 'create' do
+ let(:oldrev) { blank_sha }
+
+ it { clears_cache }
+ end
+
+ context 'update' do
+ it { clears_cache }
+ end
+
+ context 'remove' do
+ let(:newrev) { blank_sha }
+
+ it { clears_cache }
+ end
+ end
+ end
+
+ describe 'GPG signatures' do
+ context 'when the commit has a signature' do
+ context 'when the signature is already cached' do
+ before do
+ create(:gpg_signature, commit_sha: commit.id)
+ end
+
+ it 'does not queue a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'when the signature is not yet cached' do
+ it 'queues a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker).to receive(:perform_async).with([commit.id], project.id)
+
+ service.execute
+ end
+
+ it 'can queue several commits to create the gpg signature' do
+ allow(Gitlab::Git::Commit)
+ .to receive(:shas_with_signatures)
+ .and_return([sample_commit.id, another_sample_commit.id])
+
+ expect(CreateGpgSignatureWorker)
+ .to receive(:perform_async)
+ .with([sample_commit.id, another_sample_commit.id], project.id)
+
+ service.execute
+ end
+ end
+ end
+
+ context 'when the commit does not have a signature' do
+ before do
+ allow(Gitlab::Git::Commit)
+ .to receive(:shas_with_signatures)
+ .with(project.repository, [sample_commit.id])
+ .and_return([])
+ end
+
+ it 'does not queue a CreateGpgSignatureWorker' do
+ expect(CreateGpgSignatureWorker)
+ .not_to receive(:perform_async)
+ .with(sample_commit.id, project.id)
+
+ service.execute
+ end
+ end
+ end
+
+ describe 'Processing commit messages' do
+ # Create 4 commits, 2 of which have references. Limiting to 2 commits, we
+ # expect to see one commit message processor enqueued.
+ let(:commit_ids) do
+ Array.new(4) do |i|
+ message = "Issue #{'#' if i.even?}#{i}"
+ project.repository.update_file(
+ user, 'README.md', '', message: message, branch_name: branch
+ )
+ end
+ end
+
+ let(:oldrev) { commit_ids.first }
+ let(:newrev) { commit_ids.last }
+
+ before do
+ stub_const("::Git::BaseHooksService::PROCESS_COMMIT_LIMIT", 2)
+ end
+
+ context 'creating the default branch' do
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'updating the default branch' do
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'removing the default branch' do
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+
+ context 'creating a normal branch' do
+ let(:branch) { 'fix' }
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'updating a normal branch' do
+ let(:branch) { 'fix' }
+
+ it 'processes a limited number of commit messages' do
+ expect(ProcessCommitWorker).to receive(:perform_async).once
+
+ service.execute
+ end
+ end
+
+ context 'removing a normal branch' do
+ let(:branch) { 'fix' }
+ let(:newrev) { Gitlab::Git::BLANK_SHA }
+
+ it 'does not process commit messages' do
+ expect(ProcessCommitWorker).not_to receive(:perform_async)
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb
index e8fce951155..6e39fa6b3c0 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git/branch_push_service_spec.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe GitPushService, services: true do
+describe Git::BranchPushService, services: true do
include RepoHelpers
set(:user) { create(:user) }
@@ -8,78 +10,13 @@ describe GitPushService, services: true do
let(:blankrev) { Gitlab::Git::BLANK_SHA }
let(:oldrev) { sample_commit.parent_id }
let(:newrev) { sample_commit.id }
- let(:ref) { 'refs/heads/master' }
+ let(:branch) { 'master' }
+ let(:ref) { "refs/heads/#{branch}" }
before do
project.add_maintainer(user)
end
- describe 'with remote mirrors' do
- let(:project) { create(:project, :repository, :remote_mirror) }
-
- subject do
- described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
- end
-
- context 'when remote mirror feature is enabled' do
- it 'fails stuck remote mirrors' do
- allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
- expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
-
- subject.execute
- end
-
- it 'updates remote mirrors' do
- expect(project).to receive(:update_remote_mirrors)
-
- subject.execute
- end
- end
-
- context 'when remote mirror feature is disabled' do
- before do
- stub_application_setting(mirror_available: false)
- end
-
- context 'with remote mirrors global setting overridden' do
- before do
- project.remote_mirror_available_overridden = true
- end
-
- it 'fails stuck remote mirrors' do
- allow(project).to receive(:update_remote_mirrors).and_return(project.remote_mirrors)
- expect(project).to receive(:mark_stuck_remote_mirrors_as_failed!)
-
- subject.execute
- end
-
- it 'updates remote mirrors' do
- expect(project).to receive(:update_remote_mirrors)
-
- subject.execute
- end
- end
-
- context 'without remote mirrors global setting overridden' do
- before do
- project.remote_mirror_available_overridden = false
- end
-
- it 'does not fails stuck remote mirrors' do
- expect(project).not_to receive(:mark_stuck_remote_mirrors_as_failed!)
-
- subject.execute
- end
-
- it 'does not updates remote mirrors' do
- expect(project).not_to receive(:update_remote_mirrors)
-
- subject.execute
- end
- end
- end
- end
-
describe 'Push branches' do
subject do
execute_service(project, user, oldrev, newrev, ref)
@@ -132,64 +69,6 @@ describe GitPushService, services: true do
end
end
- describe "Git Push Data" do
- let(:commit) { project.commit(newrev) }
-
- subject { push_data_from_service(project, user, oldrev, newrev, ref) }
-
- it { is_expected.to include(object_kind: 'push') }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
- end
-
- context "the commit" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
- end
- end
- end
-
describe "Pipelines" do
subject { execute_service(project, user, oldrev, newrev, ref) }
@@ -203,59 +82,13 @@ describe GitPushService, services: true do
end
end
- describe "Push Event" do
- context "with an existing branch" do
- let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) }
- let(:event) { Event.find_by_action(Event::PUSHED) }
+ describe "Updates merge requests" do
+ it "when pushing a new branch for the first time" do
+ expect(UpdateMergeRequestsWorker)
+ .to receive(:perform_async)
+ .with(project.id, user.id, blankrev, 'newrev', ref)
- it 'generates a push event with one commit' do
- expect(event).to be_an_instance_of(PushEvent)
- expect(event.project).to eq(project)
- expect(event.action).to eq(Event::PUSHED)
- expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
- expect(event.push_event_payload.commit_from).to eq(oldrev)
- expect(event.push_event_payload.commit_to).to eq(newrev)
- expect(event.push_event_payload.ref).to eq('master')
- expect(event.push_event_payload.commit_count).to eq(1)
- end
- end
-
- context "with a new branch" do
- let!(:new_branch_data) { push_data_from_service(project, user, Gitlab::Git::BLANK_SHA, newrev, ref) }
- let(:event) { Event.find_by_action(Event::PUSHED) }
-
- it 'generates a push event with more than one commit' do
- expect(event).to be_an_instance_of(PushEvent)
- expect(event.project).to eq(project)
- expect(event.action).to eq(Event::PUSHED)
- expect(event.push_event_payload).to be_an_instance_of(PushEventPayload)
- expect(event.push_event_payload.commit_from).to be_nil
- expect(event.push_event_payload.commit_to).to eq(newrev)
- expect(event.push_event_payload.ref).to eq('master')
- expect(event.push_event_payload.commit_count).to be > 1
- end
- end
-
- context "Updates merge requests" do
- it "when pushing a new branch for the first time" do
- expect(UpdateMergeRequestsWorker).to receive(:perform_async)
- .with(project.id, user.id, blankrev, 'newrev', ref)
- execute_service(project, user, blankrev, 'newrev', ref )
- end
- end
-
- describe 'system hooks' do
- let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) }
- let!(:system_hooks_service) { SystemHooksService.new }
-
- it "sends a system hook after pushing a branch" do
- allow(SystemHooksService).to receive(:new).and_return(system_hooks_service)
- allow(system_hooks_service).to receive(:execute_hooks)
-
- execute_service(project, user, oldrev, newrev, ref)
-
- expect(system_hooks_service).to have_received(:execute_hooks).with(push_data, :push_hooks)
- end
+ execute_service(project, user, blankrev, 'newrev', ref )
end
end
@@ -700,125 +533,64 @@ describe GitPushService, services: true do
end
end
- describe '#update_caches' do
- let(:service) do
- described_class.new(project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: ref)
- end
-
- context 'on the default branch' do
- before do
- allow(service).to receive(:default_branch?).and_return(true)
- end
-
- it 'flushes the caches of any special files that have been changed' do
- commit = double(:commit)
- diff = double(:diff, new_path: 'README.md')
-
- expect(commit).to receive(:raw_deltas)
- .and_return([diff])
-
- service.push_commits = [commit]
+ describe "CI environments" do
+ context 'create branch' do
+ let(:oldrev) { blankrev }
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, %i(readme), %i(commit_count repository_size))
+ it 'does nothing' do
+ expect(::Ci::StopEnvironmentsService).not_to receive(:new)
- service.update_caches
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- context 'on a non-default branch' do
- before do
- allow(service).to receive(:default_branch?).and_return(false)
- end
-
- it 'does not flush any conditional caches' do
- expect(ProjectCacheWorker).to receive(:perform_async)
- .with(project.id, [], %i(commit_count repository_size))
- .and_call_original
+ context 'update branch' do
+ it 'does nothing' do
+ expect(::Ci::StopEnvironmentsService).not_to receive(:new)
- service.update_caches
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- end
-
- describe '#process_commit_messages' do
- let(:service) do
- described_class.new(project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: ref)
- end
-
- it 'only schedules a limited number of commits' do
- service.push_commits = Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true))
-
- expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times
-
- service.process_commit_messages
- end
-
- it "skips commits which don't include cross-references" do
- service.push_commits = [double(:commit, to_hash: {}, matches_cross_reference_regex?: false)]
-
- expect(ProcessCommitWorker).not_to receive(:perform_async)
-
- service.process_commit_messages
- end
- end
- describe '#update_signatures' do
- let(:service) do
- described_class.new(
- project,
- user,
- oldrev: oldrev,
- newrev: newrev,
- ref: 'refs/heads/master'
- )
- end
+ context 'delete branch' do
+ let(:newrev) { blankrev }
- context 'when the commit has a signature' do
- context 'when the signature is already cached' do
- before do
- create(:gpg_signature, commit_sha: sample_commit.id)
+ it 'stops environments' do
+ expect_next_instance_of(::Ci::StopEnvironmentsService) do |stop_service|
+ expect(stop_service.project).to eq(project)
+ expect(stop_service.current_user).to eq(user)
+ expect(stop_service).to receive(:execute).with(branch)
end
- it 'does not queue a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).not_to receive(:perform_async)
-
- execute_service(project, user, oldrev, newrev, ref)
- end
+ execute_service(project, user, oldrev, newrev, ref)
end
+ end
+ end
- context 'when the signature is not yet cached' do
- it 'queues a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id], project.id)
+ describe 'Hooks' do
+ context 'run on a branch' do
+ it 'delegates to Git::BranchHooksService' do
+ expect_next_instance_of(::Git::BranchHooksService) do |hooks_service|
+ expect(hooks_service.project).to eq(project)
+ expect(hooks_service.current_user).to eq(user)
+ expect(hooks_service.params).to include(
+ oldrev: oldrev,
+ newrev: newrev,
+ ref: ref
+ )
- execute_service(project, user, oldrev, newrev, ref)
+ expect(hooks_service).to receive(:execute)
end
- it 'can queue several commits to create the gpg signature' do
- allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).and_return([sample_commit.id, another_sample_commit.id])
-
- expect(CreateGpgSignatureWorker).to receive(:perform_async).with([sample_commit.id, another_sample_commit.id], project.id)
-
- execute_service(project, user, oldrev, newrev, ref)
- end
+ execute_service(project, user, oldrev, newrev, ref)
end
end
- context 'when the commit does not have a signature' do
- before do
- allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([])
- end
+ context 'run on a tag' do
+ let(:ref) { 'refs/tags/v1.1.0' }
- it 'does not queue a CreateGpgSignatureWorker' do
- expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id)
+ it 'does nothing' do
+ expect(::Git::BranchHooksService).not_to receive(:new)
execute_service(project, user, oldrev, newrev, ref)
end
@@ -830,8 +602,4 @@ describe GitPushService, services: true do
service.execute
service
end
-
- def push_data_from_service(project, user, oldrev, newrev, ref)
- execute_service(project, user, oldrev, newrev, ref).push_data
- end
end
diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb
new file mode 100644
index 00000000000..f5938a5c708
--- /dev/null
+++ b/spec/services/git/tag_hooks_service_spec.rb
@@ -0,0 +1,152 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::TagHooksService, :service do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { "refs/tags/#{tag_name}" }
+ let(:tag_name) { 'v1.1.0' }
+
+ let(:tag) { project.repository.find_tag(tag_name) }
+ let(:commit) { tag.dereferenced_target }
+
+ let(:service) do
+ described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref)
+ end
+
+ it 'update remote mirrors' do
+ expect(service).to receive(:update_remote_mirrors).and_call_original
+
+ service.execute
+ end
+
+ describe 'System hooks' do
+ it 'Executes system hooks' do
+ push_data = service.execute
+
+ expect_next_instance_of(SystemHooksService) do |system_hooks_service|
+ expect(system_hooks_service)
+ .to receive(:execute_hooks)
+ .with(push_data, :tag_push_hooks)
+ end
+
+ service.execute
+ end
+ end
+
+ describe "Webhooks" do
+ it "executes hooks on the project" do
+ expect(project).to receive(:execute_hooks)
+
+ service.execute
+ end
+ end
+
+ describe "Pipelines" do
+ before do
+ stub_ci_pipeline_to_return_yaml_file
+ project.add_developer(user)
+ end
+
+ it "creates a new pipeline" do
+ expect { service.execute }.to change { Ci::Pipeline.count }
+
+ expect(Ci::Pipeline.last).to be_push
+ end
+ end
+
+ describe 'Push data' do
+ shared_examples_for 'tag push data expectations' do
+ subject(:push_data) { service.execute }
+ it 'has expected push data attributes' do
+ is_expected.to match a_hash_including(
+ object_kind: 'tag_push',
+ ref: ref,
+ before: oldrev,
+ after: newrev,
+ message: tag.message,
+ user_id: user.id,
+ user_name: user.name,
+ project_id: project.id
+ )
+ end
+
+ context "with repository data" do
+ subject { push_data[:repository] }
+
+ it 'has expected repository attributes' do
+ is_expected.to match a_hash_including(
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url
+ )
+ end
+ end
+
+ context "with commits" do
+ subject { push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+
+ context "the commit" do
+ subject { push_data[:commits].first }
+
+ it { is_expected.to include(timestamp: commit.date.xmlschema) }
+
+ it 'has expected commit attributes' do
+ is_expected.to match a_hash_including(
+ id: commit.id,
+ message: commit.safe_message,
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ commit.id
+ ].join('/')
+ )
+ end
+
+ context "with an author" do
+ subject { push_data[:commits].first[:author] }
+
+ it 'has expected author attributes' do
+ is_expected.to match a_hash_including(
+ name: commit.author_name,
+ email: commit.author_email
+ )
+ end
+ end
+ end
+ end
+ end
+
+ context 'annotated tag' do
+ include_examples 'tag push data expectations'
+ end
+
+ context 'lightweight tag' do
+ let(:tag_name) { 'light-tag' }
+ let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
+
+ before do
+ # Create the lightweight tag
+ rugged_repo(project.repository).tags.create(tag_name, newrev)
+
+ # Clear tag list cache
+ project.repository.expire_tags_cache
+ end
+
+ include_examples 'tag push data expectations'
+ end
+ end
+end
diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb
new file mode 100644
index 00000000000..418952b52da
--- /dev/null
+++ b/spec/services/git/tag_push_service_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Git::TagPushService do
+ include RepoHelpers
+ include GitHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:service) { described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
+
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { 'refs/tags/v1.1.0' }
+
+ describe "Push tags" do
+ subject do
+ service.execute
+ service
+ end
+
+ it 'flushes general cached data' do
+ expect(project.repository).to receive(:before_push_tag)
+
+ subject
+ end
+
+ it 'flushes the tags cache' do
+ expect(project.repository).to receive(:expire_tags_cache)
+
+ subject
+ end
+ end
+
+ describe 'Hooks' do
+ context 'run on a tag' do
+ it 'delegates to Git::TagHooksService' do
+ expect_next_instance_of(::Git::TagHooksService) do |hooks_service|
+ expect(hooks_service.project).to eq(service.project)
+ expect(hooks_service.current_user).to eq(service.current_user)
+ expect(hooks_service.params).to eq(service.params)
+
+ expect(hooks_service).to receive(:execute)
+ end
+
+ service.execute
+ end
+ end
+
+ context 'run on a branch' do
+ let(:ref) { 'refs/heads/master' }
+
+ it 'does nothing' do
+ expect(::Git::BranchHooksService).not_to receive(:new)
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
deleted file mode 100644
index 2699f6e7bcd..00000000000
--- a/spec/services/git_tag_push_service_spec.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-require 'spec_helper'
-
-describe GitTagPushService do
- include RepoHelpers
- include GitHelpers
-
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:service) { described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
-
- let(:oldrev) { Gitlab::Git::BLANK_SHA }
- let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
- let(:ref) { 'refs/tags/v1.1.0' }
-
- describe "Push tags" do
- subject do
- service.execute
- service
- end
-
- it 'flushes general cached data' do
- expect(project.repository).to receive(:before_push_tag)
-
- subject
- end
-
- it 'flushes the tags cache' do
- expect(project.repository).to receive(:expire_tags_cache)
-
- subject
- end
- end
-
- describe "Pipelines" do
- subject { service.execute }
-
- before do
- stub_ci_pipeline_to_return_yaml_file
- project.add_developer(user)
- end
-
- it "creates a new pipeline" do
- expect { subject }.to change { Ci::Pipeline.count }
- expect(Ci::Pipeline.last).to be_push
- end
- end
-
- describe "Git Tag Push Data" do
- subject { @push_data }
- let(:tag) { project.repository.find_tag(tag_name) }
- let(:commit) { tag.dereferenced_target }
-
- context 'annotated tag' do
- let(:tag_name) { Gitlab::Git.ref_name(ref) }
-
- before do
- service.execute
- @push_data = service.push_data
- end
-
- it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(message: tag.message) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { @push_data[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { @push_data[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
- end
-
- context "the commit" do
- subject { @push_data[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { is_expected.to include(timestamp: commit.date.xmlschema) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { @push_data[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
- end
- end
- end
-
- context 'lightweight tag' do
- let(:tag_name) { 'light-tag' }
- let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
- let(:ref) { "refs/tags/light-tag" }
-
- before do
- # Create the lightweight tag
- rugged_repo(project.repository).tags.create(tag_name, newrev)
-
- # Clear tag list cache
- project.repository.expire_tags_cache
-
- service.execute
- @push_data = service.push_data
- end
-
- it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(message: tag.message) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { @push_data[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
-
- context "with commits" do
- subject { @push_data[:commits] }
-
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
- end
-
- context "the commit" do
- subject { @push_data[:commits].first }
-
- it { is_expected.to include(id: commit.id) }
- it { is_expected.to include(message: commit.safe_message) }
- it { is_expected.to include(timestamp: commit.date.xmlschema) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- commit.id
- ].join('/')
- )
- end
-
- context "with a author" do
- subject { @push_data[:commits].first[:author] }
-
- it { is_expected.to include(name: commit.author_name) }
- it { is_expected.to include(email: commit.author_email) }
- end
- end
- end
- end
- end
-
- describe "Webhooks" do
- context "execute webhooks" do
- let(:service) { described_class.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') }
-
- it "when pushing tags" do
- expect(project).to receive(:execute_hooks)
- service.execute
- end
- end
- end
-end
diff --git a/spec/services/gpg_keys/create_service_spec.rb b/spec/services/gpg_keys/create_service_spec.rb
index 1cd2625531e..8dfc9f19439 100644
--- a/spec/services/gpg_keys/create_service_spec.rb
+++ b/spec/services/gpg_keys/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GpgKeys::CreateService do
diff --git a/spec/services/gravatar_service_spec.rb b/spec/services/gravatar_service_spec.rb
index d2cc53fe0ee..9ce1df0f76f 100644
--- a/spec/services/gravatar_service_spec.rb
+++ b/spec/services/gravatar_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GravatarService do
diff --git a/spec/services/groups/auto_devops_service_spec.rb b/spec/services/groups/auto_devops_service_spec.rb
new file mode 100644
index 00000000000..7f8ab517cef
--- /dev/null
+++ b/spec/services/groups/auto_devops_service_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Groups::AutoDevopsService, '#execute' do
+ set(:group) { create(:group) }
+ set(:user) { create(:user) }
+ let(:group_params) { { auto_devops_enabled: '0' } }
+ let(:service) { described_class.new(group, user, group_params) }
+
+ context 'when user does not have enough privileges' do
+ it 'raises exception' do
+ group.add_developer(user)
+
+ expect do
+ service.execute
+ end.to raise_exception(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'when user has enough privileges' do
+ before do
+ group.add_owner(user)
+ end
+
+ it 'updates group auto devops enabled accordingly' do
+ service.execute
+
+ expect(group.auto_devops_enabled).to eq(false)
+ end
+
+ context 'when group has projects' do
+ it 'reflects changes on projects' do
+ project_1 = create(:project, namespace: group)
+
+ service.execute
+
+ expect(project_1).not_to have_auto_devops_implicitly_enabled
+ end
+ end
+
+ context 'when group has subgroups' do
+ it 'reflects changes on subgroups' do
+ subgroup_1 = create(:group, parent: group)
+
+ service.execute
+
+ expect(subgroup_1.auto_devops_enabled?).to eq(false)
+ end
+
+ context 'when subgroups have projects', :nested_groups do
+ it 'reflects changes on projects' do
+ subgroup_1 = create(:group, parent: group)
+ project_1 = create(:project, namespace: subgroup_1)
+
+ service.execute
+
+ expect(project_1).not_to have_auto_devops_implicitly_enabled
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index fe6a8691ae0..c5ff6cdbacd 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::CreateService, '#execute' do
@@ -88,6 +90,17 @@ describe Groups::CreateService, '#execute' do
end
end
+ describe "when visibility level is passed as a string" do
+ let(:service) { described_class.new(user, group_params) }
+ let(:group_params) { { path: 'group_path', visibility: 'public' } }
+
+ it "assigns the correct visibility level" do
+ group = service.execute
+
+ expect(group.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
describe 'creating a mattermost team' do
let!(:params) { group_params.merge(create_chat_team: "true") }
let!(:service) { described_class.new(user, params) }
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index d80d0f5a8a8..1ab2e994b7e 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::DestroyService do
@@ -82,44 +84,6 @@ describe Groups::DestroyService do
expect(Group.unscoped.count).to eq(2)
end
end
-
- context 'potential race conditions' do
- context "when the `GroupDestroyWorker` task runs immediately" do
- it "deletes the group" do
- # Commit the contents of this spec's transaction so far
- # so subsequent db connections can see it.
- #
- # DO NOT REMOVE THIS LINE, even if you see a WARNING with "No
- # transaction is currently in progress". Without this, this
- # spec will always be green, since the group created in setup
- # cannot be seen by any other connections / threads in this spec.
- Group.connection.commit_db_transaction
-
- group_record = run_with_new_database_connection do |conn|
- conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
- end
-
- expect(group_record).not_to be_nil
-
- # Execute the contents of `GroupDestroyWorker` in a separate thread, to
- # simulate data manipulation by the Sidekiq worker (different database
- # connection / transaction).
- expect(GroupDestroyWorker).to receive(:perform_async).and_wrap_original do |m, group_id, user_id|
- Thread.new { m[group_id, user_id] }.join(5)
- end
-
- # Kick off the initial group destroy in a new thread, so that
- # it doesn't share this spec's database transaction.
- Thread.new { described_class.new(group, user).async_execute }.join(5)
-
- group_record = run_with_new_database_connection do |conn|
- conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first
- end
-
- expect(group_record).to be_nil
- end
- end
- end
end
describe 'synchronous delete' do
diff --git a/spec/services/groups/nested_create_service_spec.rb b/spec/services/groups/nested_create_service_spec.rb
index 75d6ddb0a2c..13acf9e055b 100644
--- a/spec/services/groups/nested_create_service_spec.rb
+++ b/spec/services/groups/nested_create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::NestedCreateService do
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index 6b48c993c57..b5708ebba76 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe Groups::TransferService, :postgresql do
@@ -12,11 +14,11 @@ describe Groups::TransferService, :postgresql do
allow(Group).to receive(:supports_nested_objects?).and_return(false)
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: Database is not supported.')
end
@@ -30,11 +32,11 @@ describe Groups::TransferService, :postgresql do
create(:group_member, :owner, group: new_parent_group, user: user)
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: namespace directory cannot be moved')
end
@@ -50,7 +52,7 @@ describe Groups::TransferService, :postgresql do
context 'when the group is already a root group' do
let(:group) { create(:group, :public) }
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(nil)
expect(transfer_service.error).to eq('Transfer failed: Group is already a root group.')
end
@@ -59,11 +61,11 @@ describe Groups::TransferService, :postgresql do
context 'when the user does not have the right policies' do
let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
- it "should return false" do
+ it "returns false" do
expect(transfer_service.execute(nil)).to be_falsy
end
- it "should add an error on group" do
+ it "adds an error on group" do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.")
end
@@ -76,11 +78,11 @@ describe Groups::TransferService, :postgresql do
create(:group, path: 'not-unique')
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(nil)).to be_falsy
end
- it 'should add an error on group' 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.')
end
@@ -96,17 +98,17 @@ describe Groups::TransferService, :postgresql do
group.reload
end
- it 'should update group attributes' do
+ it 'updates group attributes' do
expect(group.parent).to be_nil
end
- it 'should update group children path' do
+ it 'updates group children path' do
group.children.each do |subgroup|
expect(subgroup.full_path).to eq("#{group.path}/#{subgroup.path}")
end
end
- it 'should update group projects path' do
+ it 'updates group projects path' do
group.projects.each do |project|
expect(project.full_path).to eq("#{group.path}/#{project.path}")
end
@@ -122,11 +124,11 @@ describe Groups::TransferService, :postgresql do
context 'when the new parent group is the same as the previous parent group' do
let(:group) { create(:group, :public, :nested, parent: new_parent_group) }
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ it 'adds an error on group' do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: Group is already associated to the parent group.')
end
@@ -135,11 +137,11 @@ describe Groups::TransferService, :postgresql do
context 'when the user does not have the right policies' do
let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
- it "should return false" do
+ it "returns false" do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it "should add an error on group" do
+ it "adds an error on group" do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq("Transfer failed: You don't have enough permissions.")
end
@@ -152,11 +154,11 @@ describe Groups::TransferService, :postgresql do
create(:group, path: "not-unique", parent: new_parent_group)
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' 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.')
end
@@ -171,11 +173,11 @@ describe Groups::TransferService, :postgresql do
group.update_attribute(:path, 'foo')
end
- it 'should return false' do
+ it 'returns false' do
expect(transfer_service.execute(new_parent_group)).to be_falsy
end
- it 'should add an error on group' do
+ 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')
end
@@ -191,7 +193,7 @@ describe Groups::TransferService, :postgresql do
let(:new_parent_group) { create(:group, :public) }
let(:group) { create(:group, :private, :nested) }
- it 'should not update the visibility for the group' do
+ it 'does not update the visibility for the group' do
group.reload
expect(group.private?).to be_truthy
expect(group.visibility_level).not_to eq(new_parent_group.visibility_level)
@@ -202,27 +204,27 @@ describe Groups::TransferService, :postgresql do
let(:new_parent_group) { create(:group, :private) }
let(:group) { create(:group, :public, :nested) }
- it 'should update visibility level based on the parent group' do
+ it 'updates visibility level based on the parent group' do
group.reload
expect(group.private?).to be_truthy
expect(group.visibility_level).to eq(new_parent_group.visibility_level)
end
end
- it 'should update visibility for the group based on the parent group' do
+ it 'updates visibility for the group based on the parent group' do
expect(group.visibility_level).to eq(new_parent_group.visibility_level)
end
- it 'should update parent group to the new parent ' do
+ it 'updates parent group to the new parent' do
expect(group.parent).to eq(new_parent_group)
end
- it 'should return the group as children of the new parent' do
+ it 'returns the group as children of the new parent' do
expect(new_parent_group.children.count).to eq(1)
expect(new_parent_group.children.first).to eq(group)
end
- it 'should create a redirect for the group' do
+ it 'creates a redirect for the group' do
expect(group.redirect_routes.count).to eq(1)
end
end
@@ -236,21 +238,21 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should update subgroups path' do
+ it 'updates subgroups path' do
new_parent_path = new_parent_group.path
group.children.each do |subgroup|
expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}")
end
end
- it 'should create redirects for the subgroups' do
+ it 'creates redirects for the subgroups' do
expect(group.redirect_routes.count).to eq(1)
expect(subgroup1.redirect_routes.count).to eq(1)
expect(subgroup2.redirect_routes.count).to eq(1)
end
context 'when the new parent has a higher visibility than the children' do
- it 'should not update the children visibility' do
+ it 'does not update the children visibility' do
expect(subgroup1.private?).to be_truthy
expect(subgroup2.internal?).to be_truthy
end
@@ -261,7 +263,7 @@ describe Groups::TransferService, :postgresql do
let!(:subgroup2) { create(:group, :public, parent: group) }
let(:new_parent_group) { create(:group, :private) }
- it 'should update children visibility to match the new parent' do
+ it 'updates children visibility to match the new parent' do
group.children.each do |subgroup|
expect(subgroup.private?).to be_truthy
end
@@ -279,21 +281,21 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should update projects path' do
+ it 'updates projects path' do
new_parent_path = new_parent_group.path
group.projects.each do |project|
expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}")
end
end
- it 'should create permanent redirects for the projects' do
+ it 'creates permanent redirects for the projects' do
expect(group.redirect_routes.count).to eq(1)
expect(project1.redirect_routes.count).to eq(1)
expect(project2.redirect_routes.count).to eq(1)
end
context 'when the new parent has a higher visibility than the projects' do
- it 'should not update projects visibility' do
+ it 'does not update projects visibility' do
expect(project1.private?).to be_truthy
expect(project2.internal?).to be_truthy
end
@@ -304,7 +306,7 @@ describe Groups::TransferService, :postgresql do
let!(:project2) { create(:project, :repository, :public, namespace: group) }
let(:new_parent_group) { create(:group, :private) }
- it 'should update projects visibility to match the new parent' do
+ it 'updates projects visibility to match the new parent' do
group.projects.each do |project|
expect(project.private?).to be_truthy
end
@@ -324,21 +326,21 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should update subgroups path' do
+ it 'updates subgroups path' do
new_parent_path = new_parent_group.path
group.children.each do |subgroup|
expect(subgroup.full_path).to eq("#{new_parent_path}/#{group.path}/#{subgroup.path}")
end
end
- it 'should update projects path' do
+ it 'updates projects path' do
new_parent_path = new_parent_group.path
group.projects.each do |project|
expect(project.full_path).to eq("#{new_parent_path}/#{group.path}/#{project.name}")
end
end
- it 'should create redirect for the subgroups and projects' do
+ it 'creates redirect for the subgroups and projects' do
expect(group.redirect_routes.count).to eq(1)
expect(subgroup1.redirect_routes.count).to eq(1)
expect(subgroup2.redirect_routes.count).to eq(1)
@@ -360,7 +362,7 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should update subgroups path' do
+ it 'updates subgroups path' do
new_base_path = "#{new_parent_group.path}/#{group.path}"
group.children.each do |children|
expect(children.full_path).to eq("#{new_base_path}/#{children.path}")
@@ -372,7 +374,7 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should update projects path' do
+ it 'updates projects path' do
new_parent_path = "#{new_parent_group.path}/#{group.path}"
subgroup1.projects.each do |project|
project_full_path = "#{new_parent_path}/#{project.namespace.path}/#{project.name}"
@@ -380,7 +382,7 @@ describe Groups::TransferService, :postgresql do
end
end
- it 'should create redirect for the subgroups and projects' do
+ it 'creates redirect for the subgroups and projects' do
expect(group.redirect_routes.count).to eq(1)
expect(project1.redirect_routes.count).to eq(1)
expect(subgroup1.redirect_routes.count).to eq(1)
@@ -402,7 +404,7 @@ describe Groups::TransferService, :postgresql do
transfer_service.execute(new_parent_group)
end
- it 'should restore group and projects visibility' do
+ it 'restores group and projects visibility' do
subgroup1.reload
project1.reload
expect(subgroup1.public?).to be_truthy
@@ -410,5 +412,34 @@ describe Groups::TransferService, :postgresql do
end
end
end
+
+ context 'when transferring a subgroup into root group' do
+ let(:group) { create(:group, :public, :nested) }
+ let(:subgroup) { create(:group, :public, parent: group) }
+ let(:transfer_service) { described_class.new(subgroup, user) }
+
+ it 'ensures there is still an owner for the transferred group' do
+ expect(subgroup.owners).to be_empty
+
+ transfer_service.execute(nil)
+ subgroup.reload
+
+ expect(subgroup.owners).to match_array(user)
+ end
+
+ context 'when group has explicit owner' do
+ let(:another_owner) { create(:user) }
+ let!(:another_member) { create(:group_member, :owner, group: subgroup, user: another_owner) }
+
+ it 'does not add additional owner' do
+ expect(subgroup.owners).to match_array(another_owner)
+
+ transfer_service.execute(nil)
+ subgroup.reload
+
+ expect(subgroup.owners).to match_array(another_owner)
+ end
+ end
+ end
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index d87a7dd234d..d081c20f669 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Groups::UpdateService do
diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb
index d5fcef1246f..51720e786dc 100644
--- a/spec/services/import_export_clean_up_service_spec.rb
+++ b/spec/services/import_export_clean_up_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ImportExportCleanUpService do
diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index ca366cdf1df..aebc5ba2874 100644
--- a/spec/services/issuable/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issuable::BulkUpdateService do
@@ -76,14 +78,14 @@ describe Issuable::BulkUpdateService do
end
describe 'updating merge request assignee' do
- let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) }
+ let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignees: [user]) }
context 'when the new assignee ID is a valid user' do
it 'succeeds' do
new_assignee = create(:user)
project.add_developer(new_assignee)
- result = bulk_update(merge_request, assignee_id: new_assignee.id)
+ result = bulk_update(merge_request, assignee_ids: [user.id, new_assignee.id])
expect(result[:success]).to be_truthy
expect(result[:count]).to eq(1)
@@ -93,22 +95,22 @@ describe Issuable::BulkUpdateService do
assignee = create(:user)
project.add_developer(assignee)
- expect { bulk_update(merge_request, assignee_id: assignee.id) }
- .to change { merge_request.reload.assignee }.from(user).to(assignee)
+ expect { bulk_update(merge_request, assignee_ids: [assignee.id]) }
+ .to change { merge_request.reload.assignee_ids }.from([user.id]).to([assignee.id])
end
end
context "when the new assignee ID is #{IssuableFinder::NONE}" do
it 'unassigns the issues' do
- expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) }
- .to change { merge_request.reload.assignee }.to(nil)
+ expect { bulk_update(merge_request, assignee_ids: [IssuableFinder::NONE]) }
+ .to change { merge_request.reload.assignee_ids }.to([])
end
end
context 'when the new assignee ID is not present' do
it 'does not unassign' do
- expect { bulk_update(merge_request, assignee_id: nil) }
- .not_to change { merge_request.reload.assignee }
+ expect { bulk_update(merge_request, assignee_ids: []) }
+ .not_to change { merge_request.reload.assignee_ids }
end
end
end
diff --git a/spec/services/issuable/clone/content_rewriter_spec.rb b/spec/services/issuable/clone/content_rewriter_spec.rb
index 4d3cb0bd254..230e1123280 100644
--- a/spec/services/issuable/clone/content_rewriter_spec.rb
+++ b/spec/services/issuable/clone/content_rewriter_spec.rb
@@ -149,5 +149,21 @@ describe Issuable::Clone::ContentRewriter do
expect(new_note.author).to eq(note.author)
end
end
+
+ context 'notes with upload' do
+ let(:uploader) { build(:file_uploader, project: project1) }
+ let(:text) { "Simple text with image: #{uploader.markdown_link} "}
+ let!(:note) { create(:note, noteable: original_issue, note: text, project: project1) }
+
+ it 'rewrites note content correctly' do
+ subject.execute
+ new_note = new_issue.notes.first
+
+ expect(note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/)
+ expect(new_note.note).to match(/Simple text with image: #{FileUploader::MARKDOWN_PATTERN}/)
+ expect(note.note).not_to eq(new_note.note)
+ expect(note.note_html).not_to eq(new_note.note_html)
+ end
+ end
end
end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index fa5d5ebac5c..7e40ac9ff4d 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issuable::CommonSystemNotesService do
let(:user) { create(:user) }
let(:project) { create(:project) }
- let(:issuable) { create(:issue) }
+ let(:issuable) { create(:issue, project: project) }
context 'on issuable update' do
it_behaves_like 'system note creation', { title: 'New title' }, 'changed title'
@@ -70,7 +72,7 @@ describe Issuable::CommonSystemNotesService do
end
context 'on issuable create' do
- let(:issuable) { build(:issue) }
+ let(:issuable) { build(:issue, project: project) }
subject { described_class.new(project, user).execute(issuable, old_labels: [], is_update: false) }
diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb
index 8ccbba7fa58..dd6a966c145 100644
--- a/spec/services/issuable/destroy_service_spec.rb
+++ b/spec/services/issuable/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issuable::DestroyService do
@@ -34,7 +36,7 @@ describe Issuable::DestroyService do
end
context 'when issuable is a merge request' do
- let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignee: user) }
+ let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignees: [user]) }
it 'destroys the merge request' do
expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1)
diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb
index 248e7d5a389..140b78f9b7a 100644
--- a/spec/services/issues/build_service_spec.rb
+++ b/spec/services/issues/build_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper.rb'
describe Issues::BuildService do
@@ -8,29 +10,29 @@ describe Issues::BuildService do
project.add_developer(user)
end
+ def build_issue(issue_params = {})
+ described_class.new(project, user, issue_params).execute
+ end
+
context 'for a single discussion' do
describe '#execute' do
let(:merge_request) { create(:merge_request, title: "Hello world", source_project: project) }
let(:discussion) { create(:diff_note_on_merge_request, project: project, noteable: merge_request, note: "Almost done").to_discussion }
- let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) }
- it 'references the noteable title in the issue title' do
- issue = service.execute
+ subject { build_issue(merge_request_to_resolve_discussions_of: merge_request.iid, discussion_to_resolve: discussion.id) }
- expect(issue.title).to include('Hello world')
+ it 'references the noteable title in the issue title' do
+ expect(subject.title).to include('Hello world')
end
it 'adds the note content to the description' do
- issue = service.execute
-
- expect(issue.description).to include('Almost done')
+ expect(subject.description).to include('Almost done')
end
end
end
context 'for discussions in a merge request' do
let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project) }
- let(:issue) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute }
describe '#items_for_discussions' do
it 'has an item for each discussion' do
@@ -58,36 +60,40 @@ describe Issues::BuildService do
"> That has a quote\n"\
">>>\n"
note_result = " > This is a string\n"\
+ " > \n"\
" > > with a blockquote\n"\
- " > > > That has a quote\n"
+ " > > > That has a quote\n"\
+ " > \n"
discussion = create(:diff_note_on_merge_request, note: note_text).to_discussion
expect(service.item_for_discussion(discussion)).to include(note_result)
end
end
describe '#execute' do
- it 'has the merge request reference in the title' do
- expect(issue.title).to include(merge_request.title)
- end
+ let(:base_params) { { merge_request_to_resolve_discussions_of: merge_request.iid } }
+
+ context 'without additional params' do
+ subject { build_issue(base_params) }
- it 'has the reference of the merge request in the description' do
- expect(issue.description).to include(merge_request.to_reference)
+ it 'has the merge request reference in the title' do
+ expect(subject.title).to include(merge_request.title)
+ end
+
+ it 'has the reference of the merge request in the description' do
+ expect(subject.description).to include(merge_request.to_reference)
+ end
end
- it 'does not assign title when a title was given' do
- issue = described_class.new(project, user,
- merge_request_to_resolve_discussions_of: merge_request,
- title: 'What an issue').execute
+ it 'uses provided title if title param given' do
+ issue = build_issue(base_params.merge(title: 'What an issue'))
expect(issue.title).to eq('What an issue')
end
- it 'does not assign description when a description was given' do
- issue = described_class.new(project, user,
- merge_request_to_resolve_discussions_of: merge_request,
- description: 'Fix at your earliest conveignance').execute
+ it 'uses provided description if description param given' do
+ issue = build_issue(base_params.merge(description: 'Fix at your earliest convenience'))
- expect(issue.description).to eq('Fix at your earliest conveignance')
+ expect(issue.description).to eq('Fix at your earliest convenience')
end
describe 'with multiple discussions' do
@@ -96,20 +102,20 @@ describe Issues::BuildService do
it 'mentions all the authors in the description' do
authors = merge_request.resolvable_discussions.map(&:author)
- expect(issue.description).to include(*authors.map(&:to_reference))
+ expect(build_issue(base_params).description).to include(*authors.map(&:to_reference))
end
it 'has a link for each unresolved discussion in the description' do
notes = merge_request.resolvable_discussions.map(&:first_note)
links = notes.map { |note| Gitlab::UrlBuilder.build(note) }
- expect(issue.description).to include(*links)
+ expect(build_issue(base_params).description).to include(*links)
end
it 'mentions additional notes' do
create_list(:diff_note_on_merge_request, 2, noteable: merge_request, project: merge_request.target_project, in_reply_to: diff_note)
- expect(issue.description).to include('(+2 comments)')
+ expect(build_issue(base_params).description).to include('(+2 comments)')
end
end
end
@@ -120,7 +126,7 @@ describe Issues::BuildService do
describe '#execute' do
it 'mentions the merge request in the description' do
- issue = described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid).execute
+ issue = build_issue(merge_request_to_resolve_discussions_of: merge_request.iid)
expect(issue.description).to include("Review the conversation in #{merge_request.to_reference}")
end
@@ -128,20 +134,18 @@ describe Issues::BuildService do
end
describe '#execute' do
- let(:milestone) { create(:milestone, project: project) }
-
it 'builds a new issues with given params' do
- issue = described_class.new(
- project,
- user,
- title: 'Issue #1',
- description: 'Issue description',
- milestone_id: milestone.id
- ).execute
-
- expect(issue.title).to eq('Issue #1')
- expect(issue.description).to eq('Issue description')
+ 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)
+
+ expect(issue.milestone).to be_nil
+ end
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 5e38d0aeb6a..6874a8a0929 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -1,11 +1,15 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issues::CloseService do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user, email: "user@example.com") }
+ let(:user2) { create(:user, email: "user2@example.com") }
let(:guest) { create(:user) }
- let(:issue) { create(:issue, assignees: [user2], author: create(:user)) }
- let(:project) { issue.project }
+ let(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: create(:user)) }
+ let(:closing_merge_request) { create(:merge_request, source_project: project) }
+ let(:closing_commit) { create(:commit, project: project) }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
before do
@@ -37,7 +41,7 @@ describe Issues::CloseService do
.and_return(true)
expect(service).to receive(:close_issue)
- .with(issue, commit: nil, notifications: true, system_note: true)
+ .with(issue, closed_via: nil, notifications: true, system_note: true)
service.execute(issue)
end
@@ -55,6 +59,38 @@ describe Issues::CloseService do
end
describe '#close_issue' do
+ context "closed by a merge request" do
+ before do
+ perform_enqueued_jobs do
+ described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request)
+ end
+ end
+
+ it 'mentions closure via a merge request' do
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(issue.title)
+ expect(email.body.parts.map(&:body)).to all(include(closing_merge_request.to_reference))
+ end
+ end
+
+ context "closed by a commit" do
+ before do
+ perform_enqueued_jobs do
+ described_class.new(project, user).close_issue(issue, closed_via: closing_commit)
+ end
+ end
+
+ it 'mentions closure via a commit' do
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(issue.title)
+ expect(email.body.parts.map(&:body)).to all(include(closing_commit.id))
+ end
+ end
+
context "valid params" do
before do
perform_enqueued_jobs do
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 715b1168bfb..b7bedc2f97e 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issues::CreateService do
@@ -138,6 +140,20 @@ describe Issues::CreateService do
end
end
+ context 'when duplicate label titles are given' do
+ let(:label) { create(:label, project: project) }
+
+ let(:opts) do
+ { title: 'Title',
+ description: 'Description',
+ labels: [label.title, label.title] }
+ end
+
+ it 'assigns the label once' do
+ expect(issue.labels).to contain_exactly(label)
+ end
+ end
+
it 'executes issue hooks when issue is not confidential' do
opts = { title: 'Title', description: 'Description', confidential: false }
diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb
index 089e77cc88b..2c9581b993c 100644
--- a/spec/services/issues/duplicate_service_spec.rb
+++ b/spec/services/issues/duplicate_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issues::DuplicateService do
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index 1e088bc7d9b..7483688d631 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issues::MoveService do
diff --git a/spec/services/issues/related_branches_service_spec.rb b/spec/services/issues/related_branches_service_spec.rb
index c2e1eba6a63..eae35f12560 100644
--- a/spec/services/issues/related_branches_service_spec.rb
+++ b/spec/services/issues/related_branches_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issues::RelatedBranchesService do
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 2a56075419b..f04029e64aa 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issues::ReopenService do
diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb
index b6cfc09da65..f12a3820b8d 100644
--- a/spec/services/issues/resolve_discussions_spec.rb
+++ b/spec/services/issues/resolve_discussions_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper.rb'
describe Issues::ResolveDiscussions do
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 931e47d3a77..22f5607cb9c 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1,4 +1,6 @@
# coding: utf-8
+# frozen_string_literal: true
+
require 'spec_helper'
describe Issues::UpdateService, :mailer do
@@ -356,7 +358,7 @@ describe Issues::UpdateService, :mailer do
it_behaves_like 'system notes for milestones'
it 'sends notifications for subscribers of changed milestone' do
- issue.milestone = create(:milestone)
+ issue.milestone = create(:milestone, project: project)
issue.save
@@ -380,7 +382,7 @@ describe Issues::UpdateService, :mailer do
end
it 'marks todos as done' do
- update_issue(milestone: create(:milestone))
+ update_issue(milestone: create(:milestone, project: project))
expect(todo.reload.done?).to eq true
end
@@ -389,7 +391,7 @@ describe Issues::UpdateService, :mailer do
it 'sends notifications for subscribers of changed milestone' do
perform_enqueued_jobs do
- update_issue(milestone: create(:milestone))
+ update_issue(milestone: create(:milestone, project: project))
end
should_email(subscriber)
@@ -592,6 +594,16 @@ describe Issues::UpdateService, :mailer do
expect(result.label_ids).not_to include(label.id)
end
end
+
+ context 'when duplicate label titles are given' do
+ let(:params) do
+ { labels: [label3.title, label3.title] }
+ end
+
+ it 'assigns the label once' do
+ expect(result.labels).to contain_exactly(label3)
+ end
+ end
end
context 'updating asssignee_id' do
diff --git a/spec/services/keys/create_service_spec.rb b/spec/services/keys/create_service_spec.rb
index bcb436c1e46..1f8b402cf08 100644
--- a/spec/services/keys/create_service_spec.rb
+++ b/spec/services/keys/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Keys::CreateService do
diff --git a/spec/services/keys/destroy_service_spec.rb b/spec/services/keys/destroy_service_spec.rb
index 28ac72ddd42..ca4bbd50c03 100644
--- a/spec/services/keys/destroy_service_spec.rb
+++ b/spec/services/keys/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Keys::DestroyService do
diff --git a/spec/services/keys/last_used_service_spec.rb b/spec/services/keys/last_used_service_spec.rb
index 8e553c2f1fa..c675df39f4d 100644
--- a/spec/services/keys/last_used_service_spec.rb
+++ b/spec/services/keys/last_used_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Keys::LastUsedService do
diff --git a/spec/services/labels/available_labels_service_spec.rb b/spec/services/labels/available_labels_service_spec.rb
new file mode 100644
index 00000000000..4d5c87ecc53
--- /dev/null
+++ b/spec/services/labels/available_labels_service_spec.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Labels::AvailableLabelsService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, group: group) }
+ let(:group) { create(:group) }
+
+ let(:project_label) { create(:label, project: project) }
+ let(:other_project_label) { create(:label) }
+ let(:group_label) { create(:group_label, group: group) }
+ let(:other_group_label) { create(:group_label) }
+ let(:labels) { [project_label, other_project_label, group_label, other_group_label] }
+
+ context '#find_or_create_by_titles' do
+ let(:label_titles) { labels.map(&:title).push('non existing title') }
+
+ context 'when parent is a project' do
+ context 'when a user is not a project member' do
+ it 'returns only relevant label ids' do
+ result = described_class.new(user, project, labels: label_titles).find_or_create_by_titles
+
+ expect(result).to match_array([project_label, group_label])
+ end
+ end
+
+ context 'when a user is a project member' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'creates new labels for not found titles' do
+ result = described_class.new(user, project, labels: label_titles).find_or_create_by_titles
+
+ expect(result.count).to eq(5)
+ expect(result).to include(project_label, group_label)
+ expect(result).not_to include(other_project_label, other_group_label)
+ end
+ end
+ end
+
+ context 'when parent is a group' do
+ context 'when a user is not a group member' do
+ it 'returns only relevant label ids' do
+ result = described_class.new(user, group, labels: label_titles).find_or_create_by_titles
+
+ expect(result).to match_array([group_label])
+ end
+ end
+
+ context 'when a user is a group member' do
+ before do
+ group.add_developer(user)
+ end
+
+ it 'creates new labels for not found titles' do
+ result = described_class.new(user, group, labels: label_titles).find_or_create_by_titles
+
+ expect(result.count).to eq(5)
+ expect(result).to include(group_label)
+ expect(result).not_to include(project_label, other_project_label, other_group_label)
+ end
+ end
+ end
+ end
+
+ context '#filter_labels_ids_in_param' do
+ let(:label_ids) { labels.map(&:id).push(99999) }
+
+ context 'when parent is a project' do
+ it 'returns only relevant label ids' do
+ result = described_class.new(user, project, ids: label_ids).filter_labels_ids_in_param(:ids)
+
+ expect(result).to match_array([project_label.id, group_label.id])
+ end
+ end
+
+ context 'when parent is a group' do
+ it 'returns only relevant label ids' do
+ result = described_class.new(user, group, ids: label_ids).filter_labels_ids_in_param(:ids)
+
+ expect(result).to match_array([group_label.id])
+ end
+ end
+ end
+end
diff --git a/spec/services/labels/create_service_spec.rb b/spec/services/labels/create_service_spec.rb
index 438e6dbc628..f057c4401e7 100644
--- a/spec/services/labels/create_service_spec.rb
+++ b/spec/services/labels/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Labels::CreateService do
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
index 7af514a5bea..438d895392b 100644
--- a/spec/services/labels/find_or_create_service_spec.rb
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Labels::FindOrCreateService do
diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb
index c4c7f33e36a..d86281b751c 100644
--- a/spec/services/labels/promote_service_spec.rb
+++ b/spec/services/labels/promote_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Labels::PromoteService do
diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb
index 80bac590a11..e29c6aeef5b 100644
--- a/spec/services/labels/transfer_service_spec.rb
+++ b/spec/services/labels/transfer_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Labels::TransferService do
diff --git a/spec/services/labels/update_service_spec.rb b/spec/services/labels/update_service_spec.rb
index c3fe33045fa..045e8af1135 100644
--- a/spec/services/labels/update_service_spec.rb
+++ b/spec/services/labels/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Labels::UpdateService do
diff --git a/spec/services/lfs/file_transformer_spec.rb b/spec/services/lfs/file_transformer_spec.rb
index e8938338cb7..888eea6e91e 100644
--- a/spec/services/lfs/file_transformer_spec.rb
+++ b/spec/services/lfs/file_transformer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe Lfs::FileTransformer do
@@ -62,6 +64,25 @@ describe Lfs::FileTransformer do
expect(result.encoding).to eq('text')
end
+ context 'when an actual file is passed' do
+ let(:file) { Tempfile.new(file_path) }
+
+ before do
+ file.write(file_content)
+ file.rewind
+ end
+
+ after do
+ file.unlink
+ end
+
+ it "creates an LfsObject with the file's content" do
+ subject.new_file(file_path, file)
+
+ expect(LfsObject.last.file.read).to eq file_content
+ end
+ end
+
context "when doesn't use LFS" do
let(:file_path) { 'other.filetype' }
diff --git a/spec/services/lfs/lock_file_service_spec.rb b/spec/services/lfs/lock_file_service_spec.rb
index 3e58eea2501..15dbc3231a3 100644
--- a/spec/services/lfs/lock_file_service_spec.rb
+++ b/spec/services/lfs/lock_file_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Lfs::LockFileService do
diff --git a/spec/services/lfs/locks_finder_service_spec.rb b/spec/services/lfs/locks_finder_service_spec.rb
index e409b77babf..0fc2c593d94 100644
--- a/spec/services/lfs/locks_finder_service_spec.rb
+++ b/spec/services/lfs/locks_finder_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Lfs::LocksFinderService do
diff --git a/spec/services/lfs/unlock_file_service_spec.rb b/spec/services/lfs/unlock_file_service_spec.rb
index fe42ca41633..8e36617c0d6 100644
--- a/spec/services/lfs/unlock_file_service_spec.rb
+++ b/spec/services/lfs/unlock_file_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Lfs::UnlockFileService do
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 5c30f5b6a61..f56c31e51f6 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Members::ApproveAccessRequestService do
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 3bc05182932..674fe0f666e 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Members::CreateService do
@@ -42,7 +44,18 @@ describe Members::CreateService do
result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
- expect(result[:message]).to include(project_user.username)
+ expect(result[:message]).to include("#{project_user.username}: Access level is not included in the list")
expect(project.users).not_to include project_user
end
+
+ it 'does not add a member with an existing invite' do
+ invited_member = create(:project_member, :invited, project: project)
+
+ params = { user_ids: invited_member.invite_email,
+ access_level: Gitlab::Access::GUEST }
+ result = described_class.new(user, params).execute(project)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Invite email has already been taken')
+ end
end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index d37ca13ebd2..52f9a305d8f 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Members::DestroyService do
@@ -43,9 +45,9 @@ describe Members::DestroyService do
shared_examples 'a service destroying a member with access' do
it_behaves_like 'a service destroying a member'
- it 'invalidates cached counts for todos and assigned issues and merge requests', :aggregate_failures do
+ it 'invalidates cached counts for assigned issues and merge requests', :aggregate_failures do
create(:issue, project: group_project, assignees: [member_user])
- create(:merge_request, source_project: group_project, assignee: member_user)
+ create(:merge_request, source_project: group_project, assignees: [member_user])
create(:todo, :pending, project: group_project, user: member_user)
create(:todo, :done, project: group_project, user: member_user)
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index e93ba5a85c0..2e5275eb3f2 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Members::RequestAccessService do
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
index 599ed39ca37..a8b28127df2 100644
--- a/spec/services/members/update_service_spec.rb
+++ b/spec/services/members/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Members::UpdateService do
diff --git a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
index af0a214c00f..f26b67f902d 100644
--- a/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
+++ b/spec/services/merge_requests/add_todo_when_build_fails_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::AddTodoWhenBuildFailsService do
@@ -77,6 +79,22 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
service.execute(commit_status)
end
end
+
+ context 'when build belongs to a merge request pipeline' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event,
+ ref: merge_request.merge_ref_path,
+ merge_request: merge_request,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let(:commit_status) { create(:ci_build, ref: merge_request.merge_ref_path, pipeline: pipeline) }
+
+ it 'notifies the todo service' do
+ expect(todo_service).to receive(:merge_request_build_failed).with(merge_request)
+ service.execute(commit_status)
+ end
+ end
end
describe '#close' do
@@ -106,6 +124,22 @@ describe MergeRequests::AddTodoWhenBuildFailsService do
service.close(commit_status)
end
end
+
+ context 'when build belongs to a merge request pipeline' do
+ let(:pipeline) do
+ create(:ci_pipeline, source: :merge_request_event,
+ ref: merge_request.merge_ref_path,
+ merge_request: merge_request,
+ merge_requests_as_head_pipeline: [merge_request])
+ end
+
+ let(:commit_status) { create(:ci_build, ref: merge_request.merge_ref_path, pipeline: pipeline) }
+
+ it 'notifies the todo service' do
+ expect(todo_service).to receive(:merge_request_build_retried).with(merge_request)
+ service.close(commit_status)
+ end
+ end
end
describe '#close_all' do
diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb
index bda6383a346..c0b57b9092d 100644
--- a/spec/services/merge_requests/assign_issues_service_spec.rb
+++ b/spec/services/merge_requests/assign_issues_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::AssignIssuesService do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 536d0d345a4..5c3b209086c 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::BuildService do
@@ -229,6 +231,15 @@ describe MergeRequests::BuildService do
end
end
end
+
+ context 'when a milestone is from another project' do
+ let(:milestone) { create(:milestone, project: create(:project)) }
+ let(:milestone_id) { milestone.id }
+
+ it 'sets milestone to nil' do
+ expect(merge_request.milestone).to be_nil
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index 433ffbd97f0..29b7e0f17e2 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::CloseService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, assignees: [user2], author: create(:user)) }
let(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
@@ -50,6 +52,14 @@ describe MergeRequests::CloseService do
it 'marks todos as done' do
expect(todo.reload).to be_done
end
+
+ context 'when auto merge is enabled' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+
+ it 'cancels the auto merge' do
+ expect(@merge_request).not_to be_auto_merge_enabled
+ end
+ end
end
it 'updates metrics' do
@@ -72,6 +82,14 @@ describe MergeRequests::CloseService do
.to change { project.open_merge_requests_count }.from(1).to(0)
end
+ it 'clean up environments for the merge request' do
+ expect_next_instance_of(Ci::StopEnvironmentsService) do |service|
+ expect(service).to receive(:execute_for_merge_request).with(merge_request)
+ end
+
+ described_class.new(project, user).execute(merge_request)
+ end
+
context 'current user is not authorized to close merge request' do
before do
perform_enqueued_jobs do
diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb
index c81fa95e4b7..68a9c0a8b86 100644
--- a/spec/services/merge_requests/conflicts/list_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/list_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::Conflicts::ListService do
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 7edf8a96c94..74f20094081 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::Conflicts::ResolveService do
diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb
index 393299cce00..a0ac7dba89d 100644
--- a/spec/services/merge_requests/create_from_issue_service_spec.rb
+++ b/spec/services/merge_requests/create_from_issue_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::CreateFromIssueService do
@@ -118,7 +120,7 @@ describe MergeRequests::CreateFromIssueService do
result = service.execute
- expect(result[:merge_request].assignee).to eq(user)
+ expect(result[:merge_request].assignees).to eq([user])
end
context 'when ref branch is set' do
diff --git a/spec/services/merge_requests/create_pipeline_service_spec.rb b/spec/services/merge_requests/create_pipeline_service_spec.rb
new file mode 100644
index 00000000000..9479439bde8
--- /dev/null
+++ b/spec/services/merge_requests/create_pipeline_service_spec.rb
@@ -0,0 +1,71 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::CreatePipelineService do
+ set(:project) { create(:project, :repository) }
+ set(:user) { create(:user) }
+ let(:service) { described_class.new(project, user, params) }
+ let(:params) { {} }
+
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
+ subject { service.execute(merge_request) }
+
+ before do
+ stub_ci_pipeline_yaml_file(YAML.dump(config))
+ end
+
+ let(:config) do
+ { rspec: { script: 'echo', only: ['merge_requests'] } }
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_branch: 'feature',
+ source_project: project,
+ target_branch: 'master',
+ target_project: project)
+ end
+
+ it 'creates a detached merge request pipeline' do
+ expect { subject }.to change { Ci::Pipeline.count }.by(1)
+
+ expect(subject).to be_persisted
+ expect(subject).to be_detached_merge_request_pipeline
+ end
+
+ context 'when service is called multiple times' do
+ it 'creates a pipeline once' do
+ expect do
+ service.execute(merge_request)
+ service.execute(merge_request)
+ end.to change { Ci::Pipeline.count }.by(1)
+ end
+
+ context 'when allow_duplicate option is true' do
+ let(:params) { { allow_duplicate: true } }
+
+ it 'creates pipelines multiple times' do
+ expect do
+ service.execute(merge_request)
+ service.execute(merge_request)
+ end.to change { Ci::Pipeline.count }.by(2)
+ end
+ end
+ end
+
+ context 'when .gitlab-ci.yml does not have only: [merge_requests] keyword' do
+ let(:config) do
+ { rspec: { script: 'echo' } }
+ end
+
+ it 'does not create a pipeline' do
+ expect { subject }.not_to change { Ci::Pipeline.count }
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index b46aa65818d..ed48f4b1e44 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::CreateService do
@@ -32,7 +34,7 @@ describe MergeRequests::CreateService do
expect(merge_request).to be_valid
expect(merge_request.work_in_progress?).to be(false)
expect(merge_request.title).to eq('Awesome merge_request')
- expect(merge_request.assignee).to be_nil
+ expect(merge_request.assignees).to be_empty
expect(merge_request.merge_params['force_remove_source_branch']).to eq('1')
end
@@ -73,7 +75,7 @@ describe MergeRequests::CreateService do
description: "well this is not done yet\n/wip",
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
@@ -89,7 +91,7 @@ describe MergeRequests::CreateService do
description: "well this is not done yet\n/wip",
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
@@ -106,11 +108,11 @@ describe MergeRequests::CreateService do
description: 'please fix',
source_branch: 'feature',
target_branch: 'master',
- assignee: assignee
+ assignees: [assignee]
}
end
- it { expect(merge_request.assignee).to eq assignee }
+ it { expect(merge_request.assignees).to eq([assignee]) }
it 'creates a todo for new assignee' do
attributes = {
@@ -173,7 +175,7 @@ describe MergeRequests::CreateService do
end
end
- describe 'Merge request pipelines' do
+ describe 'Pipelines for merge requests' do
before do
stub_ci_pipeline_yaml_file(YAML.dump(config))
end
@@ -189,12 +191,46 @@ describe MergeRequests::CreateService do
}
end
- it 'creates a merge request pipeline and sets it as a head pipeline' do
+ it 'creates a detached merge request pipeline and sets it as a head pipeline' do
expect(merge_request).to be_persisted
merge_request.reload
- expect(merge_request.merge_request_pipelines.count).to eq(1)
- expect(merge_request.actual_head_pipeline).to be_merge_request
+ expect(merge_request.pipelines_for_merge_request.count).to eq(1)
+ expect(merge_request.actual_head_pipeline).to be_detached_merge_request_pipeline
+ end
+
+ context 'when merge request is submitted from forked project' do
+ let(:target_project) { fork_project(project, nil, repository: true) }
+
+ let(:opts) do
+ {
+ title: 'Awesome merge_request',
+ source_branch: 'feature',
+ target_branch: 'master',
+ target_project_id: target_project.id
+ }
+ end
+
+ before do
+ target_project.add_developer(assignee)
+ target_project.add_maintainer(user)
+ end
+
+ it 'create legacy detached merge request pipeline for fork merge request' do
+ expect(merge_request.actual_head_pipeline)
+ .to be_legacy_detached_merge_request_pipeline
+ end
+ end
+
+ context 'when ci_use_merge_request_ref feature flag is false' do
+ before do
+ stub_feature_flags(ci_use_merge_request_ref: false)
+ end
+
+ it 'create legacy detached merge request pipeline for non-fork merge request' do
+ expect(merge_request.actual_head_pipeline)
+ .to be_legacy_detached_merge_request_pipeline
+ end
end
context 'when there are no commits between source branch and target branch' do
@@ -207,11 +243,11 @@ describe MergeRequests::CreateService do
}
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(merge_request).to be_persisted
merge_request.reload
- expect(merge_request.merge_request_pipelines.count).to eq(0)
+ expect(merge_request.pipelines_for_merge_request.count).to eq(0)
end
end
@@ -225,21 +261,8 @@ describe MergeRequests::CreateService do
merge_request
end
- it 'sets the latest merge request pipeline as the head pipeline' do
- expect(merge_request.actual_head_pipeline).to be_merge_request
- end
- end
-
- context "when the 'ci_merge_request_pipeline' feature flag is disabled" do
- before do
- stub_feature_flags(ci_merge_request_pipeline: false)
- end
-
- it 'does not create a merge request pipeline' do
- expect(merge_request).to be_persisted
-
- merge_request.reload
- expect(merge_request.merge_request_pipelines.count).to eq(0)
+ it 'sets the latest detached merge request pipeline as the head pipeline' do
+ expect(merge_request.actual_head_pipeline).to be_merge_request_event
end
end
end
@@ -254,11 +277,11 @@ describe MergeRequests::CreateService do
}
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect(merge_request).to be_persisted
merge_request.reload
- expect(merge_request.merge_request_pipelines.count).to eq(0)
+ expect(merge_request.pipelines_for_merge_request.count).to eq(0)
end
end
end
@@ -280,7 +303,7 @@ describe MergeRequests::CreateService do
let(:opts) do
{
- assignee_id: create(:user).id,
+ assignee_ids: create(:user).id,
milestone_id: 1,
title: 'Title',
description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"),
@@ -296,7 +319,7 @@ describe MergeRequests::CreateService do
it 'assigns and sets milestone to issuable from command' do
expect(merge_request).to be_persisted
- expect(merge_request.assignee).to eq(assignee)
+ expect(merge_request.assignees).to eq([assignee])
expect(merge_request.milestone).to eq(milestone)
end
end
@@ -311,28 +334,28 @@ describe MergeRequests::CreateService do
end
it 'removes assignee_id when user id is invalid' do
- opts = { title: 'Title', description: 'Description', assignee_id: -1 }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [-1] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'removes assignee_id when user id is 0' do
- opts = { title: 'Title', description: 'Description', assignee_id: 0 }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [0] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'saves assignee when user id is valid' do
project.add_maintainer(assignee)
- opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
merge_request = described_class.new(project, user, opts).execute
- expect(merge_request.assignee).to eq(assignee)
+ expect(merge_request.assignees).to eq([assignee])
end
context 'when assignee is set' do
@@ -340,7 +363,7 @@ describe MergeRequests::CreateService do
{
title: 'Title',
description: 'Description',
- assignee_id: assignee.id,
+ assignee_ids: [assignee.id],
source_branch: 'feature',
target_branch: 'master'
}
@@ -366,7 +389,7 @@ describe MergeRequests::CreateService do
levels.each do |level|
it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do
project.update(visibility_level: level)
- opts = { title: 'Title', description: 'Description', assignee_id: assignee.id }
+ opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] }
merge_request = described_class.new(project, user, opts).execute
diff --git a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
index 6268c149fc6..a1c86467f34 100644
--- a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
+++ b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_shared_state do
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index fe673de46aa..3b1096c51cb 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::FfMergeService do
@@ -7,7 +9,7 @@ describe MergeRequests::FfMergeService do
create(:merge_request,
source_branch: 'flatten-dir',
target_branch: 'improve/awesome',
- assignee: user2,
+ assignees: [user2],
author: create(:user))
end
let(:project) { merge_request.project }
@@ -72,7 +74,7 @@ describe MergeRequests::FfMergeService do
it 'logs and saves error if there is an PreReceiveError exception' do
error_message = 'error message'
- allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, error_message)
+ allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "GitLab: #{error_message}")
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb
index 274624aa8bb..0933c6d4336 100644
--- a/spec/services/merge_requests/get_urls_service_spec.rb
+++ b/spec/services/merge_requests/get_urls_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe MergeRequests::GetUrlsService do
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 04a62aa454d..2fbe5468b21 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::MergeService do
set(:user) { create(:user) }
set(:user2) { create(:user) }
- let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) }
+ let(:merge_request) { create(:merge_request, :simple, author: user2, assignees: [user2]) }
let(:project) { merge_request.project }
before do
@@ -111,7 +113,7 @@ describe MergeRequests::MergeService do
end
context 'closes related todos' do
- let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user], author: user) }
let(:project) { merge_request.project }
let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
let!(:todo) do
@@ -224,10 +226,22 @@ describe MergeRequests::MergeService do
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end
+ it 'logs and saves error if user is not authorized' do
+ unauthorized_user = create(:user)
+ project.add_reporter(unauthorized_user)
+
+ service = described_class.new(project, unauthorized_user)
+
+ service.execute(merge_request)
+
+ expect(merge_request.merge_error)
+ .to eq('You are not allowed to merge this merge request')
+ end
+
it 'logs and saves error if there is an PreReceiveError exception' do
error_message = 'error message'
- allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, error_message)
+ allow(service).to receive(:repository).and_raise(Gitlab::Git::PreReceiveError, "GitLab: #{error_message}")
allow(service).to receive(:execute_hooks)
service.execute(merge_request)
diff --git a/spec/services/merge_requests/merge_to_ref_service_spec.rb b/spec/services/merge_requests/merge_to_ref_service_spec.rb
new file mode 100644
index 00000000000..5d492e4b013
--- /dev/null
+++ b/spec/services/merge_requests/merge_to_ref_service_spec.rb
@@ -0,0 +1,174 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::MergeToRefService do
+ shared_examples_for 'MergeService for target ref' do
+ it 'target_ref has the same state of target branch' do
+ repo = merge_request.target_project.repository
+
+ process_merge_to_ref
+ merge_service.execute(merge_request)
+
+ ref_commits = repo.commits(merge_request.merge_ref_path, limit: 3)
+ target_branch_commits = repo.commits(merge_request.target_branch, limit: 3)
+
+ ref_commits.zip(target_branch_commits).each do |ref_commit, target_branch_commit|
+ expect(ref_commit.parents).to eq(target_branch_commit.parents)
+ end
+ end
+ end
+
+ shared_examples_for 'successfully merges to ref with merge method' do
+ it 'writes commit to merge ref' do
+ repository = project.repository
+ target_ref = merge_request.merge_ref_path
+
+ expect(repository.ref_exists?(target_ref)).to be(false)
+
+ result = service.execute(merge_request)
+
+ ref_head = repository.commit(target_ref)
+
+ expect(result[:status]).to eq(:success)
+ expect(result[:commit_id]).to be_present
+ expect(repository.ref_exists?(target_ref)).to be(true)
+ expect(ref_head.sha).to eq(result[:commit_id])
+ end
+ end
+
+ shared_examples_for 'successfully evaluates pre-condition checks' do
+ it 'returns an error when the failing to process the merge' do
+ allow(project.repository).to receive(:merge_to_ref).and_return(nil)
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq('Conflicts detected during merge')
+ end
+
+ it 'does not send any mail' do
+ expect { process_merge_to_ref }.not_to change { ActionMailer::Base.deliveries.count }
+ end
+
+ it 'does not change the MR state' do
+ expect { process_merge_to_ref }.not_to change { merge_request.state }
+ end
+
+ it 'does not create notes' do
+ expect { process_merge_to_ref }.not_to change { merge_request.notes.count }
+ end
+
+ it 'does not delete the source branch' do
+ expect(DeleteBranchService).not_to receive(:new)
+
+ process_merge_to_ref
+ end
+ end
+
+ set(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, :simple) }
+ let(:project) { merge_request.project }
+
+ describe '#execute' do
+ let(:service) do
+ described_class.new(project, user, commit_message: 'Awesome message',
+ should_remove_source_branch: true)
+ end
+
+ def process_merge_to_ref
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
+ end
+
+ it_behaves_like 'successfully merges to ref with merge method'
+ it_behaves_like 'successfully evaluates pre-condition checks'
+
+ context 'commit history comparison with regular MergeService' do
+ before do
+ # The merge service needs an authorized user while merge-to-ref
+ # doesn't.
+ project.add_maintainer(user)
+ end
+
+ let(:merge_ref_service) do
+ described_class.new(project, user, {})
+ end
+
+ let(:merge_service) do
+ MergeRequests::MergeService.new(project, user, {})
+ end
+
+ context 'when merge commit' do
+ it_behaves_like 'MergeService for target ref'
+ end
+
+ context 'when merge commit with squash', :quarantine do
+ before do
+ merge_request.update!(squash: true, source_branch: 'master', target_branch: 'feature')
+ end
+
+ it_behaves_like 'MergeService for target ref'
+ end
+ end
+
+ context 'merge pre-condition checks' do
+ before do
+ merge_request.project.update!(merge_method: merge_method)
+ end
+
+ context 'when semi-linear merge method' do
+ let(:merge_method) { :rebase_merge }
+
+ it_behaves_like 'successfully merges to ref with merge method'
+ it_behaves_like 'successfully evaluates pre-condition checks'
+ end
+
+ context 'when fast-forward merge method' do
+ let(:merge_method) { :ff }
+
+ it_behaves_like 'successfully merges to ref with merge method'
+ it_behaves_like 'successfully evaluates pre-condition checks'
+ end
+
+ context 'when MR is not mergeable to ref' do
+ let(:merge_method) { :merge }
+
+ it 'returns error' do
+ allow(project).to receive_message_chain(:repository, :merge_to_ref) { nil }
+
+ error_message = 'Conflicts detected during merge'
+
+ result = service.execute(merge_request)
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq(error_message)
+ end
+ end
+ end
+
+ context 'does not close related todos' do
+ let(:merge_request) { create(:merge_request, assignees: [user], author: user) }
+ let(:project) { merge_request.project }
+ let!(:todo) do
+ create(:todo, :assigned,
+ project: project,
+ author: user,
+ user: user,
+ target: merge_request)
+ end
+
+ before do
+ allow(service).to receive(:execute_hooks)
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ todo.reload
+ end
+ end
+
+ it { expect(todo).not_to be_done }
+ end
+ end
+end
diff --git a/spec/services/merge_requests/mergeability_check_service_spec.rb b/spec/services/merge_requests/mergeability_check_service_spec.rb
new file mode 100644
index 00000000000..aa0485467ed
--- /dev/null
+++ b/spec/services/merge_requests/mergeability_check_service_spec.rb
@@ -0,0 +1,187 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::MergeabilityCheckService do
+ shared_examples_for 'unmergeable merge request' do
+ it 'updates or keeps merge status as cannot_be_merged' do
+ subject
+
+ expect(merge_request.merge_status).to eq('cannot_be_merged')
+ end
+
+ it 'does not change the merge ref HEAD' do
+ expect { subject }.not_to change(merge_request, :merge_ref_head)
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ end
+ end
+
+ shared_examples_for 'mergeable merge request' do
+ it 'updates or keeps merge status as can_be_merged' do
+ subject
+
+ expect(merge_request.merge_status).to eq('can_be_merged')
+ end
+
+ it 'updates the merge ref' do
+ expect { subject }.to change(merge_request, :merge_ref_head).from(nil)
+ end
+
+ it 'returns ServiceResponse.success' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_success
+ end
+
+ it 'ServiceResponse has merge_ref_head payload' do
+ result = subject
+
+ expect(result.payload.keys).to contain_exactly(:merge_ref_head)
+ expect(result.payload[:merge_ref_head].keys)
+ .to contain_exactly(:commit_id, :target_id, :source_id)
+ end
+ end
+
+ describe '#execute' do
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, merge_status: :unchecked, source_project: project, target_project: project) }
+ let(:repo) { project.repository }
+
+ subject { described_class.new(merge_request).execute }
+
+ before do
+ project.add_developer(merge_request.author)
+ end
+
+ it_behaves_like 'mergeable merge request'
+
+ context 'when multiple calls to the service' do
+ it 'returns success' do
+ subject
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.success?).to be(true)
+ end
+
+ it 'second call does not change the merge-ref' do
+ expect { subject }.to change(merge_request, :merge_ref_head).from(nil)
+ expect { subject }.not_to change(merge_request, :merge_ref_head)
+ end
+ end
+
+ context 'when broken' do
+ before do
+ allow(merge_request).to receive(:broken?) { true }
+ allow(project.repository).to receive(:can_be_merged?) { false }
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when it has conflicts' do
+ before do
+ allow(merge_request).to receive(:broken?) { false }
+ allow(project.repository).to receive(:can_be_merged?) { false }
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when MR cannot be merged and has no merge ref' do
+ before do
+ merge_request.mark_as_unmergeable!
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when MR cannot be merged and has outdated merge ref' do
+ before do
+ MergeRequests::MergeToRefService.new(project, merge_request.author).execute(merge_request)
+ merge_request.mark_as_unmergeable!
+ end
+
+ it_behaves_like 'unmergeable merge request'
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge request is not mergeable')
+ end
+ end
+
+ context 'when merge request is not given' do
+ subject { described_class.new(nil).execute }
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.message).to eq('Invalid argument')
+ end
+ end
+
+ context 'when read only DB' do
+ it 'returns ServiceResponse.error' do
+ allow(Gitlab::Database).to receive(:read_only?) { true }
+
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.message).to eq('Unsupported operation')
+ end
+ end
+
+ context 'when MR is mergeable but merge-ref does not exists' do
+ before do
+ merge_request.mark_as_mergeable!
+ end
+
+ it 'keeps merge status as can_be_merged' do
+ expect { subject }.not_to change(merge_request, :merge_status).from('can_be_merged')
+ end
+
+ it 'returns ServiceResponse.error' do
+ result = subject
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result.error?).to be(true)
+ expect(result.message).to eq('Merge ref was not found')
+ end
+ end
+ end
+end
diff --git a/spec/services/merge_requests/migrate_external_diffs_service_spec.rb b/spec/services/merge_requests/migrate_external_diffs_service_spec.rb
new file mode 100644
index 00000000000..40ac747e66f
--- /dev/null
+++ b/spec/services/merge_requests/migrate_external_diffs_service_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::MigrateExternalDiffsService do
+ let(:merge_request) { create(:merge_request) }
+ let(:diff) { merge_request.merge_request_diff }
+
+ describe '.enqueue!', :sidekiq do
+ around do |example|
+ Sidekiq::Testing.fake! { example.run }
+ end
+
+ it 'enqueues nothing if external diffs are disabled' do
+ expect(diff).not_to be_stored_externally
+
+ expect { described_class.enqueue! }
+ .not_to change { MigrateExternalDiffsWorker.jobs.count }
+ end
+
+ it 'enqueues eligible in-database diffs if external diffs are enabled' do
+ expect(diff).not_to be_stored_externally
+
+ stub_external_diffs_setting(enabled: true)
+
+ expect { described_class.enqueue! }
+ .to change { MigrateExternalDiffsWorker.jobs.count }
+ .by(1)
+ end
+ end
+
+ describe '#execute' do
+ it 'migrates an in-database diff to the external store' do
+ expect(diff).not_to be_stored_externally
+
+ stub_external_diffs_setting(enabled: true)
+
+ described_class.new(diff).execute
+
+ expect(diff).to be_stored_externally
+ end
+ end
+end
diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb
index 5ad6f5528f9..ffc86f68469 100644
--- a/spec/services/merge_requests/post_merge_service_spec.rb
+++ b/spec/services/merge_requests/post_merge_service_spec.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::PostMergeService do
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user) }
+ let(:merge_request) { create(:merge_request, assignees: [user]) }
let(:project) { merge_request.project }
before do
@@ -60,5 +62,13 @@ describe MergeRequests::PostMergeService do
expect(merge_request.reload).to be_merged
end
+
+ it 'clean up environments for the merge request' do
+ expect_next_instance_of(Ci::StopEnvironmentsService) do |service|
+ expect(service).to receive(:execute_for_merge_request).with(merge_request)
+ end
+
+ described_class.new(project, user).execute(merge_request)
+ end
end
end
diff --git a/spec/services/merge_requests/push_options_handler_service_spec.rb b/spec/services/merge_requests/push_options_handler_service_spec.rb
new file mode 100644
index 00000000000..54b9c6dae38
--- /dev/null
+++ b/spec/services/merge_requests/push_options_handler_service_spec.rb
@@ -0,0 +1,405 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MergeRequests::PushOptionsHandlerService do
+ include ProjectForksHelper
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public, :repository) }
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:service) { described_class.new(project, user, changes, push_options) }
+ let(:source_branch) { 'fix' }
+ let(:target_branch) { 'feature' }
+ let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" }
+ let(:deleted_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 #{Gitlab::Git::BLANK_SHA} refs/heads/#{source_branch}" }
+ let(:default_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{project.default_branch}" }
+
+ before do
+ project.add_developer(user)
+ end
+
+ shared_examples_for 'a service that can create a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'creates a merge request' do
+ expect { service.execute }.to change { MergeRequest.count }.by(1)
+ end
+
+ it 'sets the correct target branch' do
+ branch = push_options[:target] || project.default_branch
+
+ service.execute
+
+ expect(last_mr.target_branch).to eq(branch)
+ end
+
+ it 'assigns the MR to the user' do
+ service.execute
+
+ expect(last_mr.assignees).to contain_exactly(user)
+ end
+
+ context 'when project has been forked' do
+ let(:forked_project) { fork_project(project, user, repository: true) }
+ let(:service) { described_class.new(forked_project, user, changes, push_options) }
+
+ before do
+ allow(forked_project).to receive(:empty_repo?).and_return(false)
+ end
+
+ it 'sets the correct source project' do
+ service.execute
+
+ expect(last_mr.source_project).to eq(forked_project)
+ end
+
+ it 'sets the correct target project' do
+ service.execute
+
+ expect(last_mr.target_project).to eq(project)
+ end
+ end
+ end
+
+ shared_examples_for 'a service that can set the target of a merge request' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'sets the target_branch' do
+ service.execute
+
+ expect(last_mr.target_branch).to eq(target_branch)
+ end
+ end
+
+ shared_examples_for 'a service that can set the merge request to merge when pipeline succeeds' do
+ subject(:last_mr) { MergeRequest.last }
+
+ it 'sets auto_merge_enabled' do
+ service.execute
+
+ expect(last_mr.auto_merge_enabled).to eq(true)
+ expect(last_mr.auto_merge_strategy).to eq(AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS)
+ end
+
+ it 'sets merge_user to the user' do
+ service.execute
+
+ expect(last_mr.merge_user).to eq(user)
+ end
+ end
+
+ shared_examples_for 'a service that does not create a merge request' do
+ it do
+ expect { service.execute }.not_to change { MergeRequest.count }
+ end
+ end
+
+ shared_examples_for 'a service that does not update a merge request' do
+ it do
+ expect { service.execute }.not_to change { MergeRequest.maximum(:updated_at) }
+ end
+ end
+
+ shared_examples_for 'a service that does nothing' do
+ include_examples 'a service that does not create a merge request'
+ include_examples 'a service that does not update a merge request'
+ end
+
+ describe '`create` push option' do
+ let(:push_options) { { create: true } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that can create a merge request'
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that can create a merge request'
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`merge_when_pipeline_succeeds` push option' do
+ let(:push_options) { { merge_when_pipeline_succeeds: true } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, merge_when_pipeline_succeeds: true } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can set the merge request to merge when pipeline succeeds'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe '`target` push option' do
+ let(:push_options) { { target: target_branch } }
+
+ context 'with a new branch' do
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, target: target_branch } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+ end
+
+ context 'with an existing branch but no open MR' do
+ let(:changes) { existing_branch_changes }
+
+ it_behaves_like 'a service that does not create a merge request'
+
+ it 'adds an error to the service' do
+ error = "A merge_request.create push option is required to create a merge request for branch #{source_branch}"
+
+ service.execute
+
+ expect(service.errors).to include(error)
+ end
+
+ context 'when coupled with the `create` push option' do
+ let(:push_options) { { create: true, target: target_branch } }
+
+ it_behaves_like 'a service that can create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+ end
+
+ context 'with an existing branch that has a merge request open' do
+ let(:changes) { existing_branch_changes }
+ let!(:merge_request) { create(:merge_request, source_project: project, source_branch: source_branch)}
+
+ it_behaves_like 'a service that does not create a merge request'
+ it_behaves_like 'a service that can set the target of a merge request'
+ end
+
+ context 'with a deleted branch' do
+ let(:changes) { deleted_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ context 'with the project default branch' do
+ let(:changes) { default_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+ end
+
+ describe 'multiple pushed branches' do
+ let(:push_options) { { create: true } }
+ let(:changes) do
+ [
+ new_branch_changes,
+ "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/feature_conflict"
+ ]
+ end
+
+ it 'creates a merge request per branch' do
+ expect { service.execute }.to change { MergeRequest.count }.by(2)
+ end
+
+ context 'when there are too many pushed branches' do
+ let(:limit) { MergeRequests::PushOptionsHandlerService::LIMIT }
+ let(:changes) do
+ TestEnv::BRANCH_SHA.to_a[0..limit].map do |x|
+ "#{Gitlab::Git::BLANK_SHA} #{x.first} refs/heads/#{x.last}"
+ end
+ end
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(["Too many branches pushed (#{limit + 1} were pushed, limit is #{limit})"])
+ end
+ end
+ end
+
+ describe 'no push options' do
+ let(:push_options) { {} }
+ let(:changes) { new_branch_changes }
+
+ it_behaves_like 'a service that does nothing'
+ end
+
+ describe 'no user' do
+ let(:user) { nil }
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(['User is required'])
+ end
+ end
+
+ describe 'unauthorized user' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ Members::DestroyService.new(user).execute(ProjectMember.find_by!(user_id: user.id))
+
+ service.execute
+
+ expect(service.errors).to eq(['User access was denied'])
+ end
+ end
+
+ describe 'handling unexpected exceptions' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+ let(:exception) { StandardError.new('My standard error') }
+
+ def run_service_with_exception
+ allow_any_instance_of(
+ MergeRequests::BuildService
+ ).to receive(:execute).and_raise(exception)
+
+ service.execute
+ end
+
+ it 'records an error' do
+ run_service_with_exception
+
+ expect(service.errors).to eq(['An unknown error occurred'])
+ end
+
+ it 'writes to Gitlab::AppLogger' do
+ expect(Gitlab::AppLogger).to receive(:error).with(exception)
+
+ run_service_with_exception
+ end
+ end
+
+ describe 'when target is not a valid branch name' do
+ let(:push_options) { { create: true, target: 'my-branch' } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ service.execute
+
+ expect(service.errors).to eq(['Branch my-branch does not exist'])
+ end
+ end
+
+ describe 'when MRs are not enabled' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'records an error' do
+ expect(project).to receive(:merge_requests_enabled?).and_return(false)
+
+ service.execute
+
+ expect(service.errors).to eq(["Merge requests are not enabled for project #{project.full_path}"])
+ end
+ end
+
+ describe 'when MR has ActiveRecord errors' do
+ let(:push_options) { { create: true } }
+ let(:changes) { new_branch_changes }
+
+ it 'adds the error to its errors property' do
+ invalid_merge_request = MergeRequest.new
+ invalid_merge_request.errors.add(:base, 'my error')
+
+ expect_any_instance_of(
+ MergeRequests::CreateService
+ ).to receive(:execute).and_return(invalid_merge_request)
+
+ service.execute
+
+ expect(service.errors).to eq(['my error'])
+ end
+ end
+end
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index 427a2d63a88..7e2f03d1097 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::RebaseService do
@@ -36,6 +38,32 @@ describe MergeRequests::RebaseService do
end
end
+ shared_examples 'sequence of failure and success' do
+ it 'properly clears the error message' do
+ allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
+
+ service.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR
+
+ allow(repository).to receive(:gitaly_operation_client).and_call_original
+
+ service.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq nil
+ end
+ end
+
+ it_behaves_like 'sequence of failure and success'
+
+ context 'with deprecated step rebase feature' do
+ before do
+ stub_feature_flags(two_step_rebase: false)
+ end
+
+ it_behaves_like 'sequence of failure and success'
+ end
+
context 'when unexpected error occurs' do
before do
allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
@@ -71,7 +99,7 @@ describe MergeRequests::RebaseService do
end
context 'valid params' do
- describe 'successful rebase' do
+ shared_examples_for 'a service that can execute a successful rebase' do
before do
service.execute(merge_request)
end
@@ -97,6 +125,22 @@ describe MergeRequests::RebaseService do
end
end
+ context 'when the two_step_rebase feature is enabled' do
+ before do
+ stub_feature_flags(two_step_rebase: true)
+ end
+
+ it_behaves_like 'a service that can execute a successful rebase'
+ end
+
+ context 'when the two_step_rebase feature is disabled' do
+ before do
+ stub_feature_flags(two_step_rebase: false)
+ end
+
+ it_behaves_like 'a service that can execute a successful rebase'
+ end
+
context 'fork' do
describe 'successful fork rebase' do
let(:forked_project) do
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 9e9dc5a576c..6ba67c7165c 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::RefreshService do
@@ -21,7 +23,8 @@ describe MergeRequests::RefreshService do
source_branch: 'master',
target_branch: 'feature',
target_project: @project,
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: @user)
@another_merge_request = create(:merge_request,
@@ -29,7 +32,8 @@ describe MergeRequests::RefreshService do
source_branch: 'master',
target_branch: 'test',
target_project: @project,
- merge_when_pipeline_succeeds: true,
+ auto_merge_enabled: true,
+ auto_merge_strategy: AutoMergeService::STRATEGY_MERGE_WHEN_PIPELINE_SUCCEEDS,
merge_user: @user)
@fork_merge_request = create(:merge_request,
@@ -81,7 +85,7 @@ describe MergeRequests::RefreshService do
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
- expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.auto_merge_enabled).to be_falsey
expect(@merge_request.diff_head_sha).to eq(@newrev)
expect(@fork_merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
@@ -97,6 +101,15 @@ describe MergeRequests::RefreshService do
}
end
+ it 'outdates MR suggestions' do
+ expect_next_instance_of(Suggestions::OutdateService) do |service|
+ expect(service).to receive(:execute).with(@merge_request).and_call_original
+ expect(service).to receive(:execute).with(@another_merge_request).and_call_original
+ end
+
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ end
+
context 'when source branch ref does not exists' do
before do
DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch)
@@ -132,12 +145,15 @@ describe MergeRequests::RefreshService do
end
end
- describe 'Merge request pipelines' do
+ describe 'Pipelines for merge requests' do
before do
stub_ci_pipeline_yaml_file(YAML.dump(config))
end
- subject { service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master') }
+ subject { service.new(project, @user).execute(@oldrev, @newrev, ref) }
+
+ let(:ref) { 'refs/heads/master' }
+ let(:project) { @project }
context "when .gitlab-ci.yml has merge_requests keywords" do
let(:config) do
@@ -150,18 +166,62 @@ describe MergeRequests::RefreshService do
}
end
- it 'create merge request pipeline with commits' do
+ it 'create detached merge request pipeline with commits' do
expect { subject }
- .to change { @merge_request.merge_request_pipelines.count }.by(1)
- .and change { @fork_merge_request.merge_request_pipelines.count }.by(1)
- .and change { @another_merge_request.merge_request_pipelines.count }.by(0)
+ .to change { @merge_request.pipelines_for_merge_request.count }.by(1)
+ .and change { @another_merge_request.pipelines_for_merge_request.count }.by(0)
expect(@merge_request.has_commits?).to be_truthy
- expect(@fork_merge_request.has_commits?).to be_truthy
expect(@another_merge_request.has_commits?).to be_falsy
end
- context "when branch pipeline was created before a merge request pipline has been created" do
+ it 'does not create detached merge request pipeline for forked project' do
+ expect { subject }
+ .not_to change { @fork_merge_request.pipelines_for_merge_request.count }
+ end
+
+ it 'create detached merge request pipeline for non-fork merge request' do
+ subject
+
+ expect(@merge_request.pipelines_for_merge_request.first)
+ .to be_detached_merge_request_pipeline
+ end
+
+ context 'when service is hooked by target branch' do
+ let(:ref) { 'refs/heads/feature' }
+
+ it 'does not create detached merge request pipeline' do
+ expect { subject }
+ .not_to change { @merge_request.pipelines_for_merge_request.count }
+ end
+ end
+
+ context 'when service runs on forked project' do
+ let(:project) { @fork_project }
+
+ it 'creates legacy detached merge request pipeline for fork merge request' do
+ expect { subject }
+ .to change { @fork_merge_request.pipelines_for_merge_request.count }.by(1)
+
+ expect(@fork_merge_request.pipelines_for_merge_request.first)
+ .to be_legacy_detached_merge_request_pipeline
+ end
+ end
+
+ context 'when ci_use_merge_request_ref feature flag is false' do
+ before do
+ stub_feature_flags(ci_use_merge_request_ref: false)
+ end
+
+ it 'create legacy detached merge request pipeline for non-fork merge request' do
+ subject
+
+ expect(@merge_request.pipelines_for_merge_request.first)
+ .to be_legacy_detached_merge_request_pipeline
+ end
+ end
+
+ context "when branch pipeline was created before a detaced merge request pipeline has been created" do
before do
create(:ci_pipeline, project: @merge_request.source_project,
sha: @merge_request.diff_head_sha,
@@ -171,38 +231,27 @@ describe MergeRequests::RefreshService do
subject
end
- it 'sets the latest merge request pipeline as a head pipeline' do
+ it 'sets the latest detached merge request pipeline as a head pipeline' do
@merge_request.reload
- expect(@merge_request.actual_head_pipeline).to be_merge_request
+ expect(@merge_request.actual_head_pipeline).to be_merge_request_event
end
it 'returns pipelines in correct order' do
@merge_request.reload
- expect(@merge_request.all_pipelines.first).to be_merge_request
+ expect(@merge_request.all_pipelines.first).to be_merge_request_event
expect(@merge_request.all_pipelines.second).to be_push
end
end
context "when MergeRequestUpdateWorker is retried by an exception" do
- it 'does not re-create a duplicate merge request pipeline' do
+ it 'does not re-create a duplicate detached merge request pipeline' do
expect do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master')
- end.to change { @merge_request.merge_request_pipelines.count }.by(1)
+ end.to change { @merge_request.pipelines_for_merge_request.count }.by(1)
expect do
service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/master')
- end.not_to change { @merge_request.merge_request_pipelines.count }
- end
- end
-
- context "when the 'ci_merge_request_pipeline' feature flag is disabled" do
- before do
- stub_feature_flags(ci_merge_request_pipeline: false)
- end
-
- it 'does not create a merge request pipeline' do
- expect { subject }
- .not_to change { @merge_request.merge_request_pipelines.count }
+ end.not_to change { @merge_request.pipelines_for_merge_request.count }
end
end
end
@@ -217,20 +266,18 @@ describe MergeRequests::RefreshService do
}
end
- it 'does not create a merge request pipeline' do
+ it 'does not create a detached merge request pipeline' do
expect { subject }
- .not_to change { @merge_request.merge_request_pipelines.count }
+ .not_to change { @merge_request.pipelines_for_merge_request.count }
end
end
end
- context 'push to origin repo source branch when an MR was reopened' do
+ context 'push to origin repo source branch' do
let(:refresh_service) { service.new(@project, @user) }
let(:notification_service) { spy('notification_service') }
before do
- @merge_request.update(state: :reopened)
-
allow(refresh_service).to receive(:execute_hooks)
allow(NotificationService).to receive(:new) { notification_service }
refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
@@ -247,7 +294,7 @@ describe MergeRequests::RefreshService do
expect(@merge_request.notes).not_to be_empty
expect(@merge_request).to be_open
- expect(@merge_request.merge_when_pipeline_succeeds).to be_falsey
+ expect(@merge_request.auto_merge_enabled).to be_falsey
expect(@merge_request.diff_head_sha).to eq(@newrev)
expect(@fork_merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
@@ -329,14 +376,16 @@ describe MergeRequests::RefreshService do
context 'push to fork repo source branch' do
let(:refresh_service) { service.new(@fork_project, @user) }
- context 'open fork merge request' do
- before do
- allow(refresh_service).to receive(:execute_hooks)
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
- reload_mrs
- end
+ def refresh
+ allow(refresh_service).to receive(:execute_hooks)
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+ end
+ context 'open fork merge request' do
it 'executes hooks with update action' do
+ refresh
+
expect(refresh_service).to have_received(:execute_hooks)
.with(@fork_merge_request, 'update', old_rev: @oldrev)
@@ -347,21 +396,30 @@ describe MergeRequests::RefreshService do
expect(@build_failed_todo).to be_pending
expect(@fork_build_failed_todo).to be_pending
end
+
+ it 'outdates opened forked MR suggestions' do
+ expect_next_instance_of(Suggestions::OutdateService) do |service|
+ expect(service).to receive(:execute).with(@fork_merge_request).and_call_original
+ end
+
+ refresh
+ end
end
context 'closed fork merge request' do
before do
@fork_merge_request.close!
- allow(refresh_service).to receive(:execute_hooks)
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
- reload_mrs
end
it 'do not execute hooks with update action' do
+ refresh
+
expect(refresh_service).not_to have_received(:execute_hooks)
end
it 'updates merge request to closed state' do
+ refresh
+
expect(@merge_request.notes).to be_empty
expect(@merge_request).to be_open
expect(@fork_merge_request.notes).to be_empty
@@ -418,35 +476,35 @@ describe MergeRequests::RefreshService do
end
let(:force_push_commit) { @project.commit('feature').id }
- it 'should reload a new diff for a push to the forked project' do
+ it 'reloads a new diff for a push to the forked project' do
expect do
service.new(@fork_project, @user).execute(@oldrev, first_commit, 'refs/heads/master')
reload_mrs
end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
end
- it 'should reload a new diff for a force push to the source branch' do
+ it 'reloads a new diff for a force push to the source branch' do
expect do
service.new(@fork_project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master')
reload_mrs
end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
end
- it 'should reload a new diff for a force push to the target branch' do
+ it 'reloads a new diff for a force push to the target branch' do
expect do
service.new(@project, @user).execute(@oldrev, force_push_commit, 'refs/heads/master')
reload_mrs
end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
end
- it 'should reload a new diff for a push to the target project that contains a commit in the MR' do
+ it 'reloads a new diff for a push to the target project that contains a commit in the MR' do
expect do
service.new(@project, @user).execute(@oldrev, first_commit, 'refs/heads/master')
reload_mrs
end.to change { forked_master_mr.merge_request_diffs.count }.by(1)
end
- it 'should not increase the diff count for a new push to target branch' do
+ it 'does not increase the diff count for a new push to target branch' do
new_commit = @project.repository.create_file(@user, 'new-file.txt', 'A new file',
message: 'This is a test',
branch_name: 'master')
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
index 5acd01828cb..cc21348ab11 100644
--- a/spec/services/merge_requests/reload_diffs_service_spec.rb
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::ReloadDiffsService, :use_clean_rails_memory_store_caching do
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 21e71509ed6..7a98437f724 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -1,10 +1,12 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::ReopenService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, :closed, assignees: [user2], author: create(:user)) }
let(:project) { merge_request.project }
before do
diff --git a/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
index e3fd906fe7b..0a10a9ee13b 100644
--- a/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
+++ b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::ResolvedDiscussionNotificationService do
diff --git a/spec/services/merge_requests/squash_service_spec.rb b/spec/services/merge_requests/squash_service_spec.rb
index 2713652873e..cb278eec692 100644
--- a/spec/services/merge_requests/squash_service_spec.rb
+++ b/spec/services/merge_requests/squash_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::SquashService do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 20580bf14b9..fbfcd95e204 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeRequests::UpdateService, :mailer do
@@ -13,7 +15,7 @@ describe MergeRequests::UpdateService, :mailer do
let(:merge_request) do
create(:merge_request, :simple, title: 'Old title',
description: "FYI #{user2.to_reference}",
- assignee_id: user3.id,
+ assignee_ids: [user3.id],
source_project: project,
author: create(:user))
end
@@ -48,7 +50,7 @@ describe MergeRequests::UpdateService, :mailer do
{
title: 'New title',
description: 'Also please fix',
- assignee_id: user2.id,
+ assignee_ids: [user.id],
state_event: 'close',
label_ids: [label.id],
target_branch: 'target',
@@ -71,7 +73,7 @@ describe MergeRequests::UpdateService, :mailer do
it 'matches base expectations' do
expect(@merge_request).to be_valid
expect(@merge_request.title).to eq('New title')
- expect(@merge_request.assignee).to eq(user2)
+ expect(@merge_request.assignees).to match_array([user])
expect(@merge_request).to be_closed
expect(@merge_request.labels.count).to eq(1)
expect(@merge_request.labels.first.title).to eq(label.name)
@@ -106,7 +108,7 @@ describe MergeRequests::UpdateService, :mailer do
note = find_note('assigned to')
expect(note).not_to be_nil
- expect(note.note).to include "assigned to #{user2.to_reference}"
+ expect(note.note).to include "assigned to #{user.to_reference} and unassigned #{user3.to_reference}"
end
it 'creates a resource label event' do
@@ -215,8 +217,9 @@ describe MergeRequests::UpdateService, :mailer do
head_pipeline_of: merge_request
)
- expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user)
+ expect(AutoMerge::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user, {})
.and_return(service_mock)
+ allow(service_mock).to receive(:available_for?) { true }
expect(service_mock).to receive(:execute).with(merge_request)
end
@@ -293,7 +296,7 @@ describe MergeRequests::UpdateService, :mailer do
context 'when is reassigned' do
before do
- update_merge_request({ assignee: user2 })
+ update_merge_request({ assignee_ids: [user2.id] })
end
it 'marks previous assignee pending todos as done' do
@@ -328,7 +331,7 @@ describe MergeRequests::UpdateService, :mailer do
it_behaves_like 'system notes for milestones'
it 'sends notifications for subscribers of changed milestone' do
- merge_request.milestone = create(:milestone)
+ merge_request.milestone = create(:milestone, project: project)
merge_request.save
@@ -352,7 +355,7 @@ describe MergeRequests::UpdateService, :mailer do
end
it 'marks pending todos as done' do
- update_merge_request({ milestone: create(:milestone) })
+ update_merge_request({ milestone: create(:milestone, project: project) })
expect(pending_todo.reload).to be_done
end
@@ -361,7 +364,7 @@ describe MergeRequests::UpdateService, :mailer do
it 'sends notifications for subscribers of changed milestone' do
perform_enqueued_jobs do
- update_merge_request(milestone: create(:milestone))
+ update_merge_request(milestone: create(:milestone, project: project))
end
should_email(subscriber)
@@ -387,7 +390,7 @@ describe MergeRequests::UpdateService, :mailer do
context 'when the assignee changes' do
it 'updates open merge request counter for assignees when merge request is reassigned' do
- update_merge_request(assignee_id: user2.id)
+ update_merge_request(assignee_ids: [user2.id])
expect(user3.assigned_open_merge_requests_count).to eq 0
expect(user2.assigned_open_merge_requests_count).to eq 1
@@ -405,7 +408,7 @@ describe MergeRequests::UpdateService, :mailer do
end
end
- context 'when the issue is relabeled' do
+ context 'when the merge request is relabeled' do
let!(:non_subscriber) { create(:user) }
let!(:subscriber) { create(:user) { |u| label.toggle_subscription(u, project) } }
@@ -541,36 +544,36 @@ describe MergeRequests::UpdateService, :mailer do
end
end
- context 'updating asssignee_id' do
+ context 'updating asssignee_ids' do
it 'does not update assignee when assignee_id is invalid' do
- merge_request.update(assignee_id: user.id)
+ merge_request.update(assignee_ids: [user.id])
- update_merge_request(assignee_id: -1)
+ update_merge_request(assignee_ids: [-1])
- expect(merge_request.reload.assignee).to eq(user)
+ expect(merge_request.reload.assignees).to eq([user])
end
it 'unassigns assignee when user id is 0' do
- merge_request.update(assignee_id: user.id)
+ merge_request.update(assignee_ids: [user.id])
- update_merge_request(assignee_id: 0)
+ update_merge_request(assignee_ids: [0])
- expect(merge_request.assignee_id).to be_nil
+ expect(merge_request.assignee_ids).to be_empty
end
it 'saves assignee when user id is valid' do
- update_merge_request(assignee_id: user.id)
+ update_merge_request(assignee_ids: [user.id])
- expect(merge_request.assignee_id).to eq(user.id)
+ expect(merge_request.assignee_ids).to eq([user.id])
end
it 'does not update assignee_id when user cannot read issue' do
- non_member = create(:user)
- original_assignee = merge_request.assignee
+ non_member = create(:user)
+ original_assignees = merge_request.assignees
- update_merge_request(assignee_id: non_member.id)
+ update_merge_request(assignee_ids: [non_member.id])
- expect(merge_request.assignee_id).to eq(original_assignee.id)
+ expect(merge_request.reload.assignees).to eq(original_assignees)
end
context "when issuable feature is private" do
@@ -583,7 +586,7 @@ describe MergeRequests::UpdateService, :mailer do
feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level"
project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE)
- expect { update_merge_request(assignee_id: assignee) }.not_to change { merge_request.assignee }
+ expect { update_merge_request(assignee_ids: [assignee]) }.not_to change { merge_request.reload.assignees }
end
end
end
@@ -619,7 +622,7 @@ describe MergeRequests::UpdateService, :mailer do
end
it 'is allowed by a user that can push to the source and can update the merge request' do
- merge_request.update!(assignee: user)
+ merge_request.update!(assignees: [user])
source_project.add_developer(user)
update_merge_request(allow_collaboration: true, title: 'Updated title')
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 3f7a544ea0a..55e705063b2 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Milestones::CloseService do
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
index 0c91112026f..97f6e947539 100644
--- a/spec/services/milestones/create_service_spec.rb
+++ b/spec/services/milestones/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Milestones::CreateService do
diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb
index 9d2be30c636..3a22e4d4f92 100644
--- a/spec/services/milestones/destroy_service_spec.rb
+++ b/spec/services/milestones/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Milestones::DestroyService do
diff --git a/spec/services/milestones/promote_service_spec.rb b/spec/services/milestones/promote_service_spec.rb
index df212d912e9..22c7e9dde30 100644
--- a/spec/services/milestones/promote_service_spec.rb
+++ b/spec/services/milestones/promote_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Milestones::PromoteService do
diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb
index a6cc2251e48..e59731207a5 100644
--- a/spec/services/note_summary_spec.rb
+++ b/spec/services/note_summary_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NoteSummary do
@@ -21,16 +23,20 @@ describe NoteSummary do
describe '#note' do
it 'returns note hash' do
- expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note')
+ Timecop.freeze do
+ expect(create_note_summary.note).to eq(noteable: noteable, project: project, author: user, note: 'note',
+ created_at: Time.now)
+ end
end
context 'when noteable is a commit' do
- let(:noteable) { build(:commit) }
+ let(:noteable) { build(:commit, system_note_timestamp: Time.at(43)) }
it 'returns note hash specific to commit' do
expect(create_note_summary.note).to eq(
noteable: nil, project: project, author: user, note: 'note',
- noteable_type: 'Commit', commit_id: noteable.id
+ noteable_type: 'Commit', commit_id: noteable.id,
+ created_at: Time.at(43)
)
end
end
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index af4daff336b..984658cbd19 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Notes::BuildService do
@@ -128,37 +130,19 @@ describe Notes::BuildService do
subject { described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute }
- shared_examples 'an individual note reply' do
- it 'builds another individual note' do
- expect(subject).to be_valid
- expect(subject).to be_a(Note)
- expect(subject.discussion_id).not_to eq(note.discussion_id)
- end
- end
-
- context 'when reply_to_individual_notes is disabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: false)
- end
-
- it_behaves_like 'an individual note reply'
+ it 'sets the note up to be in reply to that note' do
+ expect(subject).to be_valid
+ expect(subject).to be_a(DiscussionNote)
+ expect(subject.discussion_id).to eq(note.discussion_id)
end
- context 'when reply_to_individual_notes is enabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: true)
- end
+ context 'when noteable does not support replies' do
+ let(:note) { create(:note_on_commit) }
- it 'sets the note up to be in reply to that note' do
+ it 'builds another individual note' do
expect(subject).to be_valid
- expect(subject).to be_a(DiscussionNote)
- expect(subject.discussion_id).to eq(note.discussion_id)
- end
-
- context 'when noteable does not support replies' do
- let(:note) { create(:note_on_commit) }
-
- it_behaves_like 'an individual note reply'
+ expect(subject).to be_a(Note)
+ expect(subject.discussion_id).not_to eq(note.discussion_id)
end
end
end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 1645b67c329..494ca95f66d 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Notes::CreateService do
@@ -220,6 +222,19 @@ describe Notes::CreateService do
expect(note.note).to eq "HELLO\nWORLD"
end
end
+
+ context 'when note only have commands' do
+ it 'adds commands applied message to note errors' do
+ note_text = %(/close)
+ service = double(:service)
+ allow(Issues::UpdateService).to receive(:new).and_return(service)
+ expect(service).to receive(:execute)
+
+ note = described_class.new(project, user, opts.merge(note: note_text)).execute
+
+ expect(note.errors[:commands_only]).to be_present
+ end
+ end
end
context 'as a user who cannot update the target' do
@@ -285,41 +300,20 @@ describe Notes::CreateService do
subject { described_class.new(project, user, reply_opts).execute }
- context 'when reply_to_individual_notes is disabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: false)
- end
-
- it 'creates an individual note' do
- expect(subject.type).to eq(nil)
- expect(subject.discussion_id).not_to eq(existing_note.discussion_id)
- end
-
- it 'does not convert existing note' do
- expect { subject }.not_to change { existing_note.reload.type }
- end
+ it 'creates a DiscussionNote in reply to existing note' do
+ expect(subject).to be_a(DiscussionNote)
+ expect(subject.discussion_id).to eq(existing_note.discussion_id)
end
- context 'when reply_to_individual_notes is enabled' do
- before do
- stub_feature_flags(reply_to_individual_notes: true)
- end
-
- it 'creates a DiscussionNote in reply to existing note' do
- expect(subject).to be_a(DiscussionNote)
- expect(subject.discussion_id).to eq(existing_note.discussion_id)
- end
+ it 'converts existing note to DiscussionNote' do
+ expect do
+ existing_note
- it 'converts existing note to DiscussionNote' do
- expect do
- existing_note
+ Timecop.freeze(Time.now + 1.minute) { subject }
- Timecop.freeze(Time.now + 1.minute) { subject }
-
- existing_note.reload
- end.to change { existing_note.type }.from(nil).to('DiscussionNote')
- .and change { existing_note.updated_at }
- end
+ existing_note.reload
+ end.to change { existing_note.type }.from(nil).to('DiscussionNote')
+ .and change { existing_note.updated_at }
end
end
end
diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb
index b1f4e87e8ea..9faf1299ef2 100644
--- a/spec/services/notes/destroy_service_spec.rb
+++ b/spec/services/notes/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Notes::DestroyService do
diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb
index 5aae0d711c3..99db7897664 100644
--- a/spec/services/notes/post_process_service_spec.rb
+++ b/spec/services/notes/post_process_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Notes::PostProcessService do
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index 14d62763a5b..7cb0bd41f13 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Notes::QuickActionsService do
@@ -28,8 +30,8 @@ describe Notes::QuickActionsService do
end
it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do
- content, command_params = service.extract_commands(note)
- service.execute(command_params, note)
+ content, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
expect(content).to eq ''
expect(note.noteable).to be_closed
@@ -47,8 +49,8 @@ describe Notes::QuickActionsService do
let(:note_text) { '/reopen' }
it 'opens the noteable, and leave no note' do
- content, command_params = service.extract_commands(note)
- service.execute(command_params, note)
+ content, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
expect(content).to eq ''
expect(note.noteable).to be_open
@@ -59,8 +61,8 @@ describe Notes::QuickActionsService do
let(:note_text) { '/spend 1h' }
it 'updates the spent time on the noteable' do
- content, command_params = service.extract_commands(note)
- service.execute(command_params, note)
+ content, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
expect(content).to eq ''
expect(note.noteable.time_spent).to eq(3600)
@@ -75,8 +77,8 @@ describe Notes::QuickActionsService do
end
it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
- content, command_params = service.extract_commands(note)
- service.execute(command_params, note)
+ content, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
expect(content).to eq "HELLO\nWORLD"
expect(note.noteable).to be_closed
@@ -94,8 +96,8 @@ describe Notes::QuickActionsService do
let(:note_text) { "HELLO\n/reopen\nWORLD" }
it 'opens the noteable' do
- content, command_params = service.extract_commands(note)
- service.execute(command_params, note)
+ content, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
expect(content).to eq "HELLO\nWORLD"
expect(note.noteable).to be_open
@@ -185,13 +187,14 @@ describe Notes::QuickActionsService do
end
before do
+ stub_licensed_features(multiple_issue_assignees: false)
project.add_maintainer(maintainer)
project.add_maintainer(assignee)
end
it 'adds only one assignee from the list' do
- _, command_params = service.extract_commands(note)
- service.execute(command_params, note)
+ _, update_params = service.execute(note)
+ service.apply_updates(update_params, note)
expect(note.noteable.assignees.count).to eq(1)
end
diff --git a/spec/services/notes/render_service_spec.rb b/spec/services/notes/render_service_spec.rb
index f771620bc0d..ad69721d876 100644
--- a/spec/services/notes/render_service_spec.rb
+++ b/spec/services/notes/render_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Notes::RenderService do
diff --git a/spec/services/notes/resolve_service_spec.rb b/spec/services/notes/resolve_service_spec.rb
index b54d40a7a5c..3f82e1dbdc0 100644
--- a/spec/services/notes/resolve_service_spec.rb
+++ b/spec/services/notes/resolve_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Notes::ResolveService do
diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb
index fd9bff46a06..717eb97fa5a 100644
--- a/spec/services/notes/update_service_spec.rb
+++ b/spec/services/notes/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Notes::UpdateService do
diff --git a/spec/services/notification_recipient_service_spec.rb b/spec/services/notification_recipient_service_spec.rb
index cea5ea125b9..9c2283f555b 100644
--- a/spec/services/notification_recipient_service_spec.rb
+++ b/spec/services/notification_recipient_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NotificationRecipientService do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 6a5a6989607..4b40c86574f 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NotificationService, :mailer do
include EmailSpec::Matchers
+ include ExternalAuthorizationServiceHelpers
include NotificationHelpers
let(:notification) { described_class.new }
@@ -125,11 +128,7 @@ describe NotificationService, :mailer do
shared_examples 'participating by assignee notification' do
it 'emails the participant' do
- if issuable.is_a?(Issue)
- issuable.assignees << participant
- else
- issuable.update_attribute(:assignee, participant)
- end
+ issuable.assignees << participant
notification_trigger
@@ -177,7 +176,7 @@ describe NotificationService, :mailer do
end
end
- context 'when recieving a non-existent method' do
+ context 'when receiving a non-existent method' do
it 'raises NoMethodError' do
expect { async.foo(key) }.to raise_error(NoMethodError)
end
@@ -620,13 +619,13 @@ describe NotificationService, :mailer do
context "merge request diff note" do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) }
+ let(:merge_request) { create(:merge_request, source_project: project, assignees: [user], author: create(:user)) }
let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
before do
build_team(note.project)
project.add_maintainer(merge_request.author)
- project.add_maintainer(merge_request.assignee)
+ merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
end
describe '#new_note' do
@@ -637,13 +636,71 @@ describe NotificationService, :mailer do
notification.new_note(note)
expect(SentNotification.last(3).map(&:recipient).map(&:id))
- .to contain_exactly(merge_request.assignee.id, merge_request.author.id, @u_watcher.id)
+ .to contain_exactly(*merge_request.assignees.pluck(:id), merge_request.author.id, @u_watcher.id)
expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id)
end
end
end
end
+ describe 'Participating project notification settings have priority over group and global settings if available' do
+ let!(:group) { create(:group) }
+ let!(:maintainer) { group.add_owner(create(:user, username: 'maintainer')).user }
+ let!(:user1) { group.add_developer(create(:user, username: 'user_with_project_and_custom_setting')).user }
+
+ let(:project) { create(:project, :public, namespace: group) }
+ let(:issue) { create :issue, project: project, assignees: [assignee], description: '' }
+
+ before do
+ reset_delivered_emails!
+
+ create_notification_setting(user1, project, :participating)
+ end
+
+ context 'custom on group' do
+ [nil, true].each do |new_issue_value|
+ value_caption = new_issue_value || 'nil'
+ it "does not send an email to user1 when a new issue is created and new_issue is set to #{value_caption}" do
+ update_custom_notification(:new_issue, user1, resource: group, value: new_issue_value)
+
+ notification.new_issue(issue, maintainer)
+ should_not_email(user1)
+ end
+ end
+ end
+
+ context 'watch on group' do
+ it 'does not send an email' do
+ user1.notification_settings_for(group).update!(level: :watch)
+
+ notification.new_issue(issue, maintainer)
+ should_not_email(user1)
+ end
+ end
+
+ context 'custom on global, global on group' do
+ it 'does not send an email' do
+ user1.notification_settings_for(nil).update!(level: :custom)
+
+ user1.notification_settings_for(group).update!(level: :global)
+
+ notification.new_issue(issue, maintainer)
+ should_not_email(user1)
+ end
+ end
+
+ context 'watch on global, global on group' do
+ it 'does not send an email' do
+ user1.notification_settings_for(nil).update!(level: :watch)
+
+ user1.notification_settings_for(group).update!(level: :global)
+
+ notification.new_issue(issue, maintainer)
+ should_not_email(user1)
+ end
+ end
+ end
+
describe 'Issues' do
let(:group) { create(:group) }
let(:project) { create(:project, :public, namespace: group) }
@@ -661,7 +718,7 @@ describe NotificationService, :mailer do
end
describe '#new_issue' do
- it do
+ it 'notifies the expected users' do
notification.new_issue(issue, @u_disabled)
should_email(assignee)
@@ -1223,11 +1280,12 @@ describe NotificationService, :mailer do
let(:group) { create(:group) }
let(:project) { create(:project, :public, :repository, namespace: group) }
let(:another_project) { create(:project, :public, namespace: group) }
- let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' }
+ let(:assignee) { create(:user) }
+ let(:merge_request) { create :merge_request, source_project: project, assignees: [assignee], description: 'cc @participant' }
before do
project.add_maintainer(merge_request.author)
- project.add_maintainer(merge_request.assignee)
+ merge_request.assignees.each { |assignee| project.add_maintainer(assignee) }
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
update_custom_notification(:new_merge_request, @u_guest_custom, resource: project)
@@ -1239,7 +1297,7 @@ describe NotificationService, :mailer do
it do
notification.new_merge_request(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
@@ -1254,9 +1312,11 @@ describe NotificationService, :mailer do
it 'adds "assigned" reason for assignee, if any' do
notification.new_merge_request(merge_request, @u_disabled)
- email = find_email_for(merge_request.assignee)
+ merge_request.assignees.each do |assignee|
+ email = find_email_for(assignee)
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ end
end
it "emails any mentioned users with the mention level" do
@@ -1347,9 +1407,9 @@ describe NotificationService, :mailer do
end
it do
- notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, [assignee])
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(merge_request.author)
should_email(@u_watcher)
should_email(@u_participant_mentioned)
@@ -1365,17 +1425,19 @@ describe NotificationService, :mailer do
end
it 'adds "assigned" reason for new assignee' do
- notification.reassigned_merge_request(merge_request, current_user, merge_request.author)
+ notification.reassigned_merge_request(merge_request, current_user, [assignee])
- email = find_email_for(merge_request.assignee)
+ merge_request.assignees.each do |assignee|
+ email = find_email_for(assignee)
- expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ expect(email).to have_header('X-GitLab-NotificationReason', NotificationReason::ASSIGNED)
+ end
end
it_behaves_like 'participating notifications' do
let(:participant) { create(:user, username: 'user-participant') }
let(:issuable) { merge_request }
- let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, merge_request.author) }
+ let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) }
end
end
@@ -1388,7 +1450,7 @@ describe NotificationService, :mailer do
it do
notification.push_to_merge_request(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_guest_custom)
should_email(@u_custom_global)
should_email(@u_participant_mentioned)
@@ -1430,7 +1492,7 @@ describe NotificationService, :mailer do
should_email(subscriber_1_to_group_label_2)
should_email(subscriber_2_to_group_label_2)
should_email(subscriber_to_label_2)
- should_not_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_not_email(assignee) }
should_not_email(merge_request.author)
should_not_email(@u_watcher)
should_not_email(@u_participant_mentioned)
@@ -1499,7 +1561,7 @@ describe NotificationService, :mailer do
it do
notification.close_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -1529,7 +1591,7 @@ describe NotificationService, :mailer do
it do
notification.merge_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_guest_watcher)
should_email(@u_guest_custom)
@@ -1581,7 +1643,7 @@ describe NotificationService, :mailer do
it do
notification.reopen_mr(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
@@ -1606,7 +1668,7 @@ describe NotificationService, :mailer do
it do
notification.resolve_all_discussions(merge_request, @u_disabled)
- should_email(merge_request.assignee)
+ merge_request.assignees.each { |assignee| should_email(assignee) }
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
@@ -1635,7 +1697,7 @@ describe NotificationService, :mailer do
end
describe '#project_was_moved' do
- it do
+ it 'notifies the expected users' do
notification.project_was_moved(project, "gitlab/gitlab")
should_email(@u_watcher)
@@ -1850,8 +1912,8 @@ describe NotificationService, :mailer do
let(:guest) { create(:user) }
let(:developer) { create(:user) }
let(:assignee) { create(:user) }
- let(:merge_request) { create(:merge_request, source_project: private_project, assignee: assignee) }
- let(:merge_request1) { create(:merge_request, source_project: private_project, assignee: assignee, description: "cc @#{guest.username}") }
+ let(:merge_request) { create(:merge_request, source_project: private_project, assignees: [assignee]) }
+ let(:merge_request1) { create(:merge_request, source_project: private_project, assignees: [assignee], description: "cc @#{guest.username}") }
let(:note) { create(:note, noteable: merge_request, project: private_project) }
before do
@@ -2217,6 +2279,46 @@ describe NotificationService, :mailer do
end
end
+ context 'with external authorization service' do
+ let(:issue) { create(:issue) }
+ let(:project) { issue.project }
+ let(:note) { create(:note, noteable: issue, project: project) }
+ let(:member) { create(:user) }
+
+ subject { NotificationService.new }
+
+ before do
+ project.add_maintainer(member)
+ member.global_notification_setting.update!(level: :watch)
+ end
+
+ it 'sends email when the service is not enabled' do
+ expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original
+
+ subject.new_issue(issue, member)
+ end
+
+ context 'when the service is enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not send an email' do
+ expect(Notify).not_to receive(:new_issue_email)
+
+ subject.new_issue(issue, member)
+ end
+
+ it 'still delivers email to admins' do
+ member.update!(admin: true)
+
+ expect(Notify).to receive(:new_issue_email).at_least(:once).with(member.id, issue.id, nil).and_call_original
+
+ subject.new_issue(issue, member)
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/pages_domains/create_acme_order_service_spec.rb b/spec/services/pages_domains/create_acme_order_service_spec.rb
new file mode 100644
index 00000000000..d59aa9b979e
--- /dev/null
+++ b/spec/services/pages_domains/create_acme_order_service_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomains::CreateAcmeOrderService do
+ include LetsEncryptHelpers
+
+ let(:pages_domain) { create(:pages_domain) }
+
+ let(:challenge) { ::Gitlab::LetsEncrypt::Challenge.new(acme_challenge_double) }
+
+ let(:order_double) do
+ Gitlab::LetsEncrypt::Order.new(acme_order_double).tap do |order|
+ allow(order).to receive(:new_challenge).and_return(challenge)
+ end
+ end
+
+ let(:lets_encrypt_client) do
+ instance_double('Gitlab::LetsEncrypt::Client').tap do |client|
+ allow(client).to receive(:new_order).with(pages_domain.domain)
+ .and_return(order_double)
+ end
+ end
+
+ let(:service) { described_class.new(pages_domain) }
+
+ before do
+ allow(::Gitlab::LetsEncrypt::Client).to receive(:new).and_return(lets_encrypt_client)
+ end
+
+ it 'saves order to database before requesting validation' do
+ allow(pages_domain.acme_orders).to receive(:create!).and_call_original
+ allow(challenge).to receive(:request_validation).and_call_original
+
+ service.execute
+
+ expect(pages_domain.acme_orders).to have_received(:create!).ordered
+ expect(challenge).to have_received(:request_validation).ordered
+ end
+
+ it 'generates and saves private key' do
+ service.execute
+
+ saved_order = PagesDomainAcmeOrder.last
+ expect { OpenSSL::PKey::RSA.new(saved_order.private_key) }.not_to raise_error
+ end
+
+ it 'properly saves order attributes' do
+ service.execute
+
+ saved_order = PagesDomainAcmeOrder.last
+ expect(saved_order.url).to eq(order_double.url)
+ expect(saved_order.expires_at).to be_like_time(order_double.expires)
+ end
+
+ it 'properly saves challenge attributes' do
+ service.execute
+
+ saved_order = PagesDomainAcmeOrder.last
+ expect(saved_order.challenge_token).to eq(challenge.token)
+ expect(saved_order.challenge_file_content).to eq(challenge.file_content)
+ end
+end
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
new file mode 100644
index 00000000000..6d7be27939c
--- /dev/null
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -0,0 +1,146 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomains::ObtainLetsEncryptCertificateService do
+ include LetsEncryptHelpers
+
+ let(:pages_domain) { create(:pages_domain, :without_certificate, :without_key) }
+ let(:service) { described_class.new(pages_domain) }
+
+ before do
+ stub_lets_encrypt_settings
+ end
+
+ def expect_to_create_acme_challenge
+ expect(::PagesDomains::CreateAcmeOrderService).to receive(:new).with(pages_domain)
+ .and_wrap_original do |m, *args|
+ create_service = m.call(*args)
+
+ expect(create_service).to receive(:execute)
+
+ create_service
+ end
+ end
+
+ def stub_lets_encrypt_order(url, status)
+ order = ::Gitlab::LetsEncrypt::Order.new(acme_order_double(status: status))
+
+ allow_any_instance_of(::Gitlab::LetsEncrypt::Client).to(
+ receive(:load_order).with(url).and_return(order)
+ )
+
+ order
+ end
+
+ context 'when there is no acme order' do
+ it 'creates acme order' do
+ expect_to_create_acme_challenge
+
+ service.execute
+ end
+ end
+
+ context 'when there is expired acme order' do
+ let!(:existing_order) do
+ create(:pages_domain_acme_order, :expired, pages_domain: pages_domain)
+ end
+
+ it 'removes acme order and creates new one' do
+ expect_to_create_acme_challenge
+
+ service.execute
+
+ expect(PagesDomainAcmeOrder.find_by_id(existing_order.id)).to be_nil
+ end
+ end
+
+ %w(pending processing).each do |status|
+ context "there is an order in '#{status}' status" do
+ let(:existing_order) do
+ create(:pages_domain_acme_order, pages_domain: pages_domain)
+ end
+
+ before do
+ stub_lets_encrypt_order(existing_order.url, status)
+ end
+
+ it 'does not raise errors' do
+ expect do
+ service.execute
+ end.not_to raise_error
+ end
+ end
+ end
+
+ context 'when order is ready' do
+ let(:existing_order) do
+ create(:pages_domain_acme_order, pages_domain: pages_domain)
+ end
+
+ let!(:api_order) do
+ stub_lets_encrypt_order(existing_order.url, 'ready')
+ end
+
+ it 'request certificate' do
+ expect(api_order).to receive(:request_certificate).and_call_original
+
+ service.execute
+ end
+ end
+
+ context 'when order is valid' do
+ let(:existing_order) do
+ create(:pages_domain_acme_order, pages_domain: pages_domain)
+ end
+
+ let!(:api_order) do
+ stub_lets_encrypt_order(existing_order.url, 'valid')
+ end
+
+ let(:certificate) do
+ key = OpenSSL::PKey.read(existing_order.private_key)
+
+ subject = "/C=BE/O=Test/OU=Test/CN=#{pages_domain.domain}"
+
+ cert = OpenSSL::X509::Certificate.new
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
+ cert.not_before = Time.now
+ cert.not_after = 1.year.from_now
+ cert.public_key = key.public_key
+ cert.serial = 0x0
+ cert.version = 2
+
+ ef = OpenSSL::X509::ExtensionFactory.new
+ ef.subject_certificate = cert
+ ef.issuer_certificate = cert
+ cert.extensions = [
+ ef.create_extension("basicConstraints", "CA:TRUE", true),
+ ef.create_extension("subjectKeyIdentifier", "hash")
+ ]
+ cert.add_extension ef.create_extension("authorityKeyIdentifier",
+ "keyid:always,issuer:always")
+
+ cert.sign key, OpenSSL::Digest::SHA1.new
+
+ cert.to_pem
+ end
+
+ before do
+ expect(api_order).to receive(:certificate) { certificate }
+ end
+
+ it 'saves private_key and certificate for domain' do
+ service.execute
+
+ expect(pages_domain.key).to be_present
+ expect(pages_domain.certificate).to eq(certificate)
+ end
+
+ it 'removes order from database' do
+ service.execute
+
+ expect(PagesDomainAcmeOrder.find_by_id(existing_order.id)).to be_nil
+ end
+ end
+end
diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb
index 85515d548a7..d25e9958831 100644
--- a/spec/services/preview_markdown_service_spec.rb
+++ b/spec/services/preview_markdown_service_spec.rb
@@ -1,8 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PreviewMarkdownService do
let(:user) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project, :repository) }
before do
project.add_developer(user)
@@ -20,23 +22,74 @@ describe PreviewMarkdownService do
end
describe 'suggestions' do
- let(:params) { { text: "```suggestion\nfoo\n```", preview_suggestions: preview_suggestions } }
+ let(:merge_request) do
+ create(:merge_request, target_project: project, source_project: project)
+ end
+ let(:text) { "```suggestion\nfoo\n```" }
+ let(:params) do
+ suggestion_params.merge(text: text,
+ target_type: 'MergeRequest',
+ target_id: merge_request.iid)
+ end
let(:service) { described_class.new(project, user, params) }
context 'when preview markdown param is present' do
- let(:preview_suggestions) { true }
+ let(:path) { "files/ruby/popen.rb" }
+ let(:line) { 10 }
+ let(:diff_refs) { merge_request.diff_refs }
+
+ let(:suggestion_params) do
+ {
+ preview_suggestions: true,
+ file_path: path,
+ line: line,
+ base_sha: diff_refs.base_sha,
+ start_sha: diff_refs.start_sha,
+ head_sha: diff_refs.head_sha
+ }
+ end
+
+ it 'returns suggestions referenced in text' do
+ position = Gitlab::Diff::Position.new(new_path: path,
+ new_line: line,
+ diff_refs: diff_refs)
+
+ expect(Gitlab::Diff::SuggestionsParser)
+ .to receive(:parse)
+ .with(text, position: position,
+ project: merge_request.project,
+ supports_suggestion: true)
+ .and_call_original
- it 'returns users referenced in text' do
result = service.execute
- expect(result[:suggestions]).to eq(['foo'])
+ expect(result[:suggestions]).to all(be_a(Gitlab::Diff::Suggestion))
+ end
+
+ context 'when user is not authorized' do
+ let(:another_user) { create(:user) }
+ let(:service) { described_class.new(project, another_user, params) }
+
+ before do
+ project.add_guest(another_user)
+ end
+
+ it 'returns no suggestions' do
+ result = service.execute
+
+ expect(result[:suggestions]).to be_empty
+ end
end
end
context 'when preview markdown param is not present' do
- let(:preview_suggestions) { false }
+ let(:suggestion_params) do
+ {
+ preview_suggestions: false
+ }
+ end
- it 'returns users referenced in text' do
+ it 'returns suggestions referenced in text' do
result = service.execute
expect(result[:suggestions]).to eq([])
@@ -49,8 +102,8 @@ describe PreviewMarkdownService do
let(:params) do
{
text: "Please do it\n/assign #{user.to_reference}",
- quick_actions_target_type: 'Issue',
- quick_actions_target_id: issue.id
+ target_type: 'Issue',
+ target_id: issue.id
}
end
let(:service) { described_class.new(project, user, params) }
@@ -72,7 +125,7 @@ describe PreviewMarkdownService do
let(:params) do
{
text: "My work\n/estimate 2y",
- quick_actions_target_type: 'MergeRequest'
+ target_type: 'MergeRequest'
}
end
let(:service) { described_class.new(project, user, params) }
@@ -96,8 +149,8 @@ describe PreviewMarkdownService do
let(:params) do
{
text: "My work\n/tag v1.2.3 Stable release",
- quick_actions_target_type: 'Commit',
- quick_actions_target_id: commit.id
+ target_type: 'Commit',
+ target_id: commit.id
}
end
let(:service) { described_class.new(project, user, params) }
diff --git a/spec/services/projects/after_import_service_spec.rb b/spec/services/projects/after_import_service_spec.rb
index 4dd6c6dab86..51d3fd18881 100644
--- a/spec/services/projects/after_import_service_spec.rb
+++ b/spec/services/projects/after_import_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::AfterImportService do
diff --git a/spec/services/projects/auto_devops/disable_service_spec.rb b/spec/services/projects/auto_devops/disable_service_spec.rb
index 76977d7a1a7..fb1ab3f9949 100644
--- a/spec/services/projects/auto_devops/disable_service_spec.rb
+++ b/spec/services/projects/auto_devops/disable_service_spec.rb
@@ -46,7 +46,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do
create(:ci_pipeline, :failed, :auto_devops_source, project: project)
end
- it 'should disable Auto DevOps for project' do
+ it 'disables Auto DevOps for project' do
subject
expect(auto_devops.enabled).to eq(false)
@@ -58,7 +58,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do
create_list(:ci_pipeline, 2, :failed, :auto_devops_source, project: project)
end
- it 'should explicitly disable Auto DevOps for project' do
+ it 'explicitly disables Auto DevOps for project' do
subject
expect(auto_devops.reload.enabled).to eq(false)
@@ -70,7 +70,7 @@ describe Projects::AutoDevops::DisableService, '#execute' do
create(:ci_pipeline, :success, :auto_devops_source, project: project)
end
- it 'should not disable Auto DevOps for project' do
+ it 'does not disable Auto DevOps for project' do
subject
expect(auto_devops.reload.enabled).to be_nil
@@ -85,14 +85,14 @@ describe Projects::AutoDevops::DisableService, '#execute' do
create(:ci_pipeline, :failed, :auto_devops_source, project: project)
end
- it 'should disable Auto DevOps for project' do
+ it 'disables Auto DevOps for project' do
subject
auto_devops = project.reload.auto_devops
expect(auto_devops.enabled).to eq(false)
end
- it 'should create a ProjectAutoDevops record' do
+ it 'creates a ProjectAutoDevops record' do
expect { subject }.to change { ProjectAutoDevops.count }.from(0).to(1)
end
end
diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb
index 373fe7cb7dd..2f70c8ea94d 100644
--- a/spec/services/projects/autocomplete_service_spec.rb
+++ b/spec/services/projects/autocomplete_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::AutocompleteService do
diff --git a/spec/services/projects/batch_open_issues_count_service_spec.rb b/spec/services/projects/batch_open_issues_count_service_spec.rb
index 599aaf62080..e978334d68b 100644
--- a/spec/services/projects/batch_open_issues_count_service_spec.rb
+++ b/spec/services/projects/batch_open_issues_count_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::BatchOpenIssuesCountService do
diff --git a/spec/services/projects/cleanup_service_spec.rb b/spec/services/projects/cleanup_service_spec.rb
index 3d4587ce2a1..5c246854eb7 100644
--- a/spec/services/projects/cleanup_service_spec.rb
+++ b/spec/services/projects/cleanup_service_spec.rb
@@ -1,16 +1,18 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CleanupService do
let(:project) { create(:project, :repository, bfg_object_map: fixture_file_upload('spec/fixtures/bfg_object_map.txt')) }
let(:object_map) { project.bfg_object_map }
+ let(:cleaner) { service.__send__(:repository_cleaner) }
+
subject(:service) { described_class.new(project) }
describe '#execute' do
- it 'runs the apply_bfg_object_map gitaly RPC' do
- expect_next_instance_of(Gitlab::Git::RepositoryCleaner) do |cleaner|
- expect(cleaner).to receive(:apply_bfg_object_map).with(kind_of(IO))
- end
+ it 'runs the apply_bfg_object_map_stream gitaly RPC' do
+ expect(cleaner).to receive(:apply_bfg_object_map_stream).with(kind_of(IO))
service.execute
end
@@ -35,10 +37,91 @@ describe Projects::CleanupService do
expect(object_map.exists?).to be_falsy
end
+ context 'with a tainted merge request diff' do
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+ let(:diff) { merge_request.merge_request_diff }
+ let(:entry) { build_entry(diff.commits.first.id) }
+
+ before do
+ allow(cleaner)
+ .to receive(:apply_bfg_object_map_stream)
+ .and_yield(Gitaly::ApplyBfgObjectMapStreamResponse.new(entries: [entry]))
+ end
+
+ it 'removes the tainted commit from the database' do
+ service.execute
+
+ expect(MergeRequestDiff.exists?(diff.id)).to be_falsy
+ end
+
+ it 'ignores non-commit responses from Gitaly' do
+ entry.type = :UNKNOWN
+
+ service.execute
+
+ expect(MergeRequestDiff.exists?(diff.id)).to be_truthy
+ end
+ end
+
+ context 'with a tainted diff note' do
+ let(:diff_note) { create(:diff_note_on_commit, project: project) }
+ let(:note_diff_file) { diff_note.note_diff_file }
+ let(:entry) { build_entry(diff_note.commit_id) }
+
+ let(:highlight_cache) { Gitlab::DiscussionsDiff::HighlightCache }
+ let(:cache_id) { note_diff_file.id }
+
+ before do
+ allow(cleaner)
+ .to receive(:apply_bfg_object_map_stream)
+ .and_yield(Gitaly::ApplyBfgObjectMapStreamResponse.new(entries: [entry]))
+ end
+
+ it 'removes the tainted commit from the database' do
+ service.execute
+
+ expect(NoteDiffFile.exists?(note_diff_file.id)).to be_falsy
+ end
+
+ it 'removes the highlight cache from redis' do
+ write_cache(highlight_cache, cache_id, [{}])
+
+ expect(read_cache(highlight_cache, cache_id)).not_to be_nil
+
+ service.execute
+
+ expect(read_cache(highlight_cache, cache_id)).to be_nil
+ end
+
+ it 'ignores non-commit responses from Gitaly' do
+ entry.type = :UNKNOWN
+
+ service.execute
+
+ expect(NoteDiffFile.exists?(note_diff_file.id)).to be_truthy
+ end
+ end
+
it 'raises an error if no object map can be found' do
object_map.remove!
expect { service.execute }.to raise_error(described_class::NoUploadError)
end
end
+
+ def build_entry(old_oid)
+ Gitaly::ApplyBfgObjectMapStreamResponse::Entry.new(
+ type: :COMMIT,
+ old_oid: old_oid,
+ new_oid: Gitlab::Git::BLANK_SHA
+ )
+ end
+
+ def read_cache(cache, key)
+ cache.read_multiple([key]).first
+ end
+
+ def write_cache(cache, key, value)
+ cache.write_multiple(key => value)
+ end
end
diff --git a/spec/services/projects/count_service_spec.rb b/spec/services/projects/count_service_spec.rb
index 183f6128c7b..e345b508f53 100644
--- a/spec/services/projects/count_service_spec.rb
+++ b/spec/services/projects/count_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CountService do
diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb
index da078dd36c6..6c244d23877 100644
--- a/spec/services/projects/create_from_template_service_spec.rb
+++ b/spec/services/projects/create_from_template_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CreateFromTemplateService do
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index d1b110b9806..f54f9200661 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::CreateService, '#execute' do
+ include ExternalAuthorizationServiceHelpers
include GitHelpers
let(:gitlab_shell) { Gitlab::Shell.new }
@@ -265,32 +268,6 @@ describe Projects::CreateService, '#execute' do
end
end
- context 'when group has kubernetes cluster' do
- let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) }
- let(:group) { group_cluster.group }
-
- let(:token) { 'aaaa' }
- let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
- let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
-
- before do
- group.add_owner(user)
-
- expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
- expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
- end
-
- it 'creates kubernetes namespace for the project' do
- project = create_project(user, opts.merge!(namespace_id: group.id))
-
- expect(project).to be_valid
-
- kubernetes_namespace = group_cluster.kubernetes_namespaces.first
- expect(kubernetes_namespace).to be_present
- expect(kubernetes_namespace.project).to eq(project)
- end
- end
-
context 'when there is an active service template' do
before do
create(:service, project: nil, template: true, active: true)
@@ -343,6 +320,42 @@ describe Projects::CreateService, '#execute' do
expect(rugged.config['gitlab.fullpath']).to eq project.full_path
end
+ context 'with external authorization enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not save the project with an error if the service denies access' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'new-label', any_args) { false }
+
+ project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' }))
+
+ expect(project.errors[:external_authorization_classification_label]).to be_present
+ expect(project).not_to be_persisted
+ end
+
+ it 'saves the project when the user has access to the label' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'new-label', any_args) { true }
+
+ project = create_project(user, opts.merge({ external_authorization_classification_label: 'new-label' }))
+
+ expect(project).to be_persisted
+ expect(project.external_authorization_classification_label).to eq('new-label')
+ end
+
+ it 'does not save the project when the user has no access to the default label and no label is provided' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'default_label', any_args) { false }
+
+ project = create_project(user, opts)
+
+ expect(project.errors[:external_authorization_classification_label]).to be_present
+ expect(project).not_to be_persisted
+ end
+ end
+
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index dfbdfa2ab69..3af7ee3ad50 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::DestroyService do
@@ -128,10 +130,8 @@ describe Projects::DestroyService do
it 'keeps project team intact upon an error' do
perform_enqueued_jobs do
- begin
- destroy_project(project, user, {})
- rescue ::Redis::CannotConnectError
- end
+ destroy_project(project, user, {})
+ rescue ::Redis::CannotConnectError
end
expect(project.team.members.count).to eq 2
diff --git a/spec/services/projects/detect_repository_languages_service_spec.rb b/spec/services/projects/detect_repository_languages_service_spec.rb
index deea1189cdf..df5eed18ac0 100644
--- a/spec/services/projects/detect_repository_languages_service_spec.rb
+++ b/spec/services/projects/detect_repository_languages_service_spec.rb
@@ -1,9 +1,11 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_state do
set(:project) { create(:project, :repository) }
- subject { described_class.new(project, project.owner) }
+ subject { described_class.new(project) }
describe '#execute' do
context 'without previous detection' do
@@ -19,6 +21,10 @@ describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_
expect(names).to eq(%w[Ruby JavaScript HTML CoffeeScript])
end
+
+ it 'updates detected_repository_languages flag' do
+ expect { subject.execute }.to change(project, :detected_repository_languages).to(true)
+ end
end
context 'with a previous detection' do
@@ -36,6 +42,12 @@ describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_
expect(repository_languages).to eq(%w[Ruby D])
end
+
+ it "doesn't touch detected_repository_languages flag" do
+ expect(project).not_to receive(:update_column).with(:detected_repository_languages, true)
+
+ subject.execute
+ end
end
context 'when no repository exists' do
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index da236052ebf..f25233ceeb1 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::DownloadService do
diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb
index 835dae68fcd..64de373d7f6 100644
--- a/spec/services/projects/enable_deploy_key_service_spec.rb
+++ b/spec/services/projects/enable_deploy_key_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::EnableDeployKeyService do
diff --git a/spec/services/projects/fetch_statistics_increment_service_spec.rb b/spec/services/projects/fetch_statistics_increment_service_spec.rb
new file mode 100644
index 00000000000..fcfb138aad6
--- /dev/null
+++ b/spec/services/projects/fetch_statistics_increment_service_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+module Projects
+ describe FetchStatisticsIncrementService do
+ let(:project) { create(:project) }
+
+ describe '#execute' do
+ subject { described_class.new(project).execute }
+
+ it 'creates a new record for today with count == 1' do
+ expect { subject }.to change { ProjectDailyStatistic.count }.by(1)
+ created_stat = ProjectDailyStatistic.last
+
+ expect(created_stat.fetch_count).to eq(1)
+ expect(created_stat.project).to eq(project)
+ expect(created_stat.date).to eq(Date.today)
+ end
+
+ it "doesn't increment previous days statistics" do
+ yesterday_stat = create(:project_daily_statistic, fetch_count: 5, project: project, date: 1.day.ago)
+
+ expect { subject }.not_to change { yesterday_stat.reload.fetch_count }
+ end
+
+ context 'when the record already exists for today' do
+ let!(:project_daily_stat) { create(:project_daily_statistic, fetch_count: 5, project: project, date: Date.today) }
+
+ it 'increments the today record count by 1' do
+ expect { subject }.to change { project_daily_stat.reload.fetch_count }.to(6)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 23ec29cce7b..3211a6e1310 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ForkService do
@@ -143,6 +145,30 @@ describe Projects::ForkService do
end
end
+ context "CI/CD settings" do
+ let(:to_project) { fork_project(@from_project, @to_user) }
+
+ context "when origin has git depth specified" do
+ before do
+ @from_project.update(default_git_depth: 42)
+ end
+
+ it "inherits default_git_depth from the origin project" do
+ expect(to_project.default_git_depth).to eq(42)
+ end
+ end
+
+ context "when origin does not define git depth" do
+ before do
+ @from_project.update!(default_git_depth: nil)
+ end
+
+ it "the fork has git depth set to 0" do
+ expect(to_project.default_git_depth).to eq(0)
+ end
+ end
+ end
+
context "when project has restricted visibility level" do
context "and only one visibility level is restricted" do
before do
diff --git a/spec/services/projects/forks_count_service_spec.rb b/spec/services/projects/forks_count_service_spec.rb
index 9f8e7ee18a8..7e35648e9ff 100644
--- a/spec/services/projects/forks_count_service_spec.rb
+++ b/spec/services/projects/forks_count_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ForksCountService do
diff --git a/spec/services/projects/git_deduplication_service_spec.rb b/spec/services/projects/git_deduplication_service_spec.rb
new file mode 100644
index 00000000000..3acbc46b473
--- /dev/null
+++ b/spec/services/projects/git_deduplication_service_spec.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::GitDeduplicationService do
+ include ExclusiveLeaseHelpers
+
+ let(:pool) { create(:pool_repository, :ready) }
+ let(:project) { create(:project, :repository) }
+ let(:lease_key) { "git_deduplication:#{project.id}" }
+ let(:lease_timeout) { Projects::GitDeduplicationService::LEASE_TIMEOUT }
+
+ subject(:service) { described_class.new(project) }
+
+ describe '#execute' do
+ context 'when there is not already a lease' do
+ context 'when the project does not have a pool repository' do
+ it 'calls disconnect_git_alternates' do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+
+ expect(project.repository).to receive(:disconnect_alternates)
+
+ service.execute
+ end
+ end
+
+ context 'when the project has a pool repository' do
+ let(:project) { create(:project, :repository, pool_repository: pool) }
+
+ context 'when the project is a source project' do
+ let(:lease_key) { "git_deduplication:#{pool.source_project.id}" }
+
+ subject(:service) { described_class.new(pool.source_project) }
+
+ it 'calls fetch' do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ allow(pool.source_project).to receive(:git_objects_poolable?).and_return(true)
+
+ expect(pool.object_pool).to receive(:fetch)
+
+ service.execute
+ end
+
+ it 'does not call fetch if git objects are not poolable' do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ allow(pool.source_project).to receive(:git_objects_poolable?).and_return(false)
+
+ expect(pool.object_pool).not_to receive(:fetch)
+
+ service.execute
+ end
+
+ it 'does not call fetch if pool and project are not on the same storage' do
+ stub_exclusive_lease(lease_key, timeout: lease_timeout)
+ allow(pool.source_project.repository).to receive(:storage).and_return('special_storage_001')
+
+ expect(pool.object_pool).not_to receive(:fetch)
+
+ service.execute
+ end
+ end
+
+ it 'links the repository to the object pool' do
+ expect(project).to receive(:link_pool_repository)
+
+ service.execute
+ end
+
+ it 'does not link the repository to the object pool if they are not on the same storage' do
+ allow(project.repository).to receive(:storage).and_return('special_storage_001')
+ expect(project).not_to receive(:link_pool_repository)
+
+ service.execute
+ end
+ end
+
+ context 'when a lease is already out' do
+ before do
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
+ end
+
+ it 'fails when a lease is already out' do
+ expect(service).to receive(:log_error).with('Cannot obtain an exclusive lease. There must be another instance already in execution.')
+
+ service.execute
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb
index b5f2c826c97..78580bfa604 100644
--- a/spec/services/projects/gitlab_projects_import_service_spec.rb
+++ b/spec/services/projects/gitlab_projects_import_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::GitlabProjectsImportService do
diff --git a/spec/services/projects/group_links/create_service_spec.rb b/spec/services/projects/group_links/create_service_spec.rb
index ffb270d277e..6ded57f961c 100644
--- a/spec/services/projects/group_links/create_service_spec.rb
+++ b/spec/services/projects/group_links/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::GroupLinks::CreateService, '#execute' do
@@ -12,6 +14,10 @@ describe Projects::GroupLinks::CreateService, '#execute' do
end
let(:subject) { described_class.new(project, user, opts) }
+ before do
+ group.add_developer(user)
+ end
+
it 'adds group to project' do
expect { subject.execute(group) }.to change { project.project_group_links.count }.from(0).to(1)
end
@@ -19,4 +25,8 @@ describe Projects::GroupLinks::CreateService, '#execute' do
it 'returns false if group is blank' do
expect { subject.execute(nil) }.not_to change { project.project_group_links.count }
end
+
+ it 'returns error if user is not allowed to share with a group' do
+ expect { subject.execute(create :group) }.not_to change { project.project_group_links.count }
+ end
end
diff --git a/spec/services/projects/group_links/destroy_service_spec.rb b/spec/services/projects/group_links/destroy_service_spec.rb
index 336ee01ae50..d78ab78c3d8 100644
--- a/spec/services/projects/group_links/destroy_service_spec.rb
+++ b/spec/services/projects/group_links/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::GroupLinks::DestroyService, '#execute' do
diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
index 61dbb57ec08..32ebec318f2 100644
--- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::HashedStorage::MigrateAttachmentsService do
@@ -70,12 +72,18 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
FileUtils.mkdir_p(base_path(hashed_storage))
end
- it 'raises AttachmentMigrationError' do
+ it 'raises AttachmentCannotMoveError' do
expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage))
- expect { service.execute }.to raise_error(Projects::HashedStorage::AttachmentMigrationError)
+ expect { service.execute }.to raise_error(Projects::HashedStorage::AttachmentCannotMoveError)
end
end
+
+ it 'works even when project validation fails' do
+ allow(project).to receive(:valid?) { false }
+
+ expect { service.execute }.to change { project.hashed_storage?(:attachments) }.to(true)
+ end
end
context '#old_disk_path' do
@@ -86,6 +94,8 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
context '#new_disk_path' do
it 'returns new disk_path for project' do
+ service.execute
+
expect(service.new_disk_path).to eq(project.disk_path)
end
end
diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
index 0772dc4b85b..5b778f16b5a 100644
--- a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::HashedStorage::MigrateRepositoryService do
@@ -28,7 +30,17 @@ describe Projects::HashedStorage::MigrateRepositoryService do
it 'fails when a git operation is in progress' do
allow(project).to receive(:repo_reference_count) { 1 }
- expect { service.execute }.to raise_error(Projects::HashedStorage::RepositoryMigrationError)
+ expect { service.execute }.to raise_error(Projects::HashedStorage::RepositoryInUseError)
+ end
+ end
+
+ context 'when repository doesnt exist on disk' do
+ let(:project) { create(:project, :legacy_storage) }
+
+ it 'skips the disk change but increase the version' do
+ service.execute
+
+ expect(project.hashed_storage?(:repository)).to be_truthy
end
end
@@ -92,6 +104,12 @@ describe Projects::HashedStorage::MigrateRepositoryService do
end
end
+ it 'works even when project validation fails' do
+ allow(project).to receive(:valid?) { false }
+
+ expect { service.execute }.to change { project.hashed_storage?(:repository) }.to(true)
+ end
+
def expect_move_repository(from_name, to_name)
expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage, from_name, to_name).and_call_original
end
diff --git a/spec/services/projects/hashed_storage/migration_service_spec.rb b/spec/services/projects/hashed_storage/migration_service_spec.rb
index b4647586363..e3191cd7ebc 100644
--- a/spec/services/projects/hashed_storage/migration_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migration_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::HashedStorage::MigrationService do
diff --git a/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb b/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb
new file mode 100644
index 00000000000..815c85e0866
--- /dev/null
+++ b/spec/services/projects/hashed_storage/rollback_attachments_service_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::HashedStorage::RollbackAttachmentsService do
+ subject(:service) { described_class.new(project, logger: nil) }
+
+ let(:project) { create(:project, :repository, skip_disk_validation: true) }
+ let(:legacy_storage) { Storage::LegacyProject.new(project) }
+ let(:hashed_storage) { Storage::HashedProject.new(project) }
+
+ let!(:upload) { Upload.find_by(path: file_uploader.upload_path) }
+ let(:file_uploader) { build(:file_uploader, project: project) }
+ let(:old_disk_path) { File.join(base_path(hashed_storage), upload.path) }
+ let(:new_disk_path) { File.join(base_path(legacy_storage), upload.path) }
+
+ context '#execute' do
+ context 'when succeeds' do
+ it 'moves attachments to legacy storage layout' do
+ expect(File.file?(old_disk_path)).to be_truthy
+ expect(File.file?(new_disk_path)).to be_falsey
+ expect(File.exist?(base_path(hashed_storage))).to be_truthy
+ expect(File.exist?(base_path(legacy_storage))).to be_falsey
+ expect(FileUtils).to receive(:mv).with(base_path(hashed_storage), base_path(legacy_storage)).and_call_original
+
+ service.execute
+
+ expect(File.exist?(base_path(legacy_storage))).to be_truthy
+ expect(File.exist?(base_path(hashed_storage))).to be_falsey
+ expect(File.file?(old_disk_path)).to be_falsey
+ expect(File.file?(new_disk_path)).to be_truthy
+ end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+
+ it 'sets skipped to false' do
+ service.execute
+
+ expect(service.skipped?).to be_falsey
+ end
+ end
+
+ context 'when original folder does not exist anymore' do
+ before do
+ FileUtils.rm_rf(base_path(hashed_storage))
+ end
+
+ it 'skips moving folders and go to next' do
+ expect(FileUtils).not_to receive(:mv).with(base_path(hashed_storage), base_path(legacy_storage))
+
+ service.execute
+
+ expect(File.exist?(base_path(legacy_storage))).to be_falsey
+ expect(File.file?(new_disk_path)).to be_falsey
+ end
+
+ it 'returns true' do
+ expect(service.execute).to be_truthy
+ end
+
+ it 'sets skipped to true' do
+ service.execute
+
+ expect(service.skipped?).to be_truthy
+ end
+ end
+
+ context 'when target folder already exists' do
+ before do
+ FileUtils.mkdir_p(base_path(legacy_storage))
+ end
+
+ it 'raises AttachmentCannotMoveError' do
+ expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage))
+
+ expect { service.execute }.to raise_error(Projects::HashedStorage::AttachmentCannotMoveError)
+ end
+ end
+
+ it 'works even when project validation fails' do
+ allow(project).to receive(:valid?) { false }
+
+ expect { service.execute }.to change { project.hashed_storage?(:attachments) }.to(false)
+ end
+ end
+
+ context '#old_disk_path' do
+ it 'returns old disk_path for project' do
+ expect(service.old_disk_path).to eq(project.disk_path)
+ end
+ end
+
+ context '#new_disk_path' do
+ it 'returns new disk_path for project' do
+ service.execute
+
+ expect(service.new_disk_path).to eq(project.full_path)
+ end
+ end
+
+ def base_path(storage)
+ File.join(FileUploader.root, storage.disk_path)
+ end
+end
diff --git a/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
new file mode 100644
index 00000000000..bd4354a7df3
--- /dev/null
+++ b/spec/services/projects/hashed_storage/rollback_repository_service_spec.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::HashedStorage::RollbackRepositoryService, :clean_gitlab_redis_shared_state do
+ include GitHelpers
+
+ let(:gitlab_shell) { Gitlab::Shell.new }
+ let(:project) { create(:project, :repository, :wiki_repo, storage_version: ::Project::HASHED_STORAGE_FEATURES[:repository]) }
+ let(:legacy_storage) { Storage::LegacyProject.new(project) }
+ let(:hashed_storage) { Storage::HashedProject.new(project) }
+
+ subject(:service) { described_class.new(project, project.disk_path) }
+
+ describe '#execute' do
+ let(:old_disk_path) { hashed_storage.disk_path }
+ let(:new_disk_path) { legacy_storage.disk_path }
+
+ before do
+ allow(service).to receive(:gitlab_shell) { gitlab_shell }
+ end
+
+ context 'repository lock' do
+ it 'tries to lock the repository' do
+ expect(service).to receive(:try_to_set_repository_read_only!)
+
+ service.execute
+ end
+
+ it 'fails when a git operation is in progress' do
+ allow(project).to receive(:repo_reference_count) { 1 }
+
+ expect { service.execute }.to raise_error(Projects::HashedStorage::RepositoryInUseError)
+ end
+ end
+
+ context 'when repository doesnt exist on disk' do
+ let(:project) { create(:project) }
+
+ it 'skips the disk change but decrease the version' do
+ service.execute
+
+ expect(project.legacy_storage?).to be_truthy
+ end
+ end
+
+ context 'when succeeds' do
+ it 'renames project and wiki repositories' do
+ service.execute
+
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_truthy
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_truthy
+ end
+
+ it 'updates project to be legacy and not read-only' do
+ service.execute
+
+ expect(project.legacy_storage?).to be_truthy
+ expect(project.repository_read_only).to be_falsey
+ end
+
+ it 'move operation is called for both repositories' do
+ expect_move_repository(old_disk_path, new_disk_path)
+ expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
+
+ service.execute
+ end
+
+ it 'writes project full path to .git/config' do
+ service.execute
+
+ rugged_config = rugged_repo(project.repository).config['gitlab.fullpath']
+
+ expect(rugged_config).to eq project.full_path
+ end
+ end
+
+ context 'when one move fails' do
+ it 'rolls repositories back to original name' do
+ allow(service).to receive(:move_repository).and_call_original
+ allow(service).to receive(:move_repository).with(old_disk_path, new_disk_path).once { false } # will disable first move only
+
+ expect(service).to receive(:rollback_folder_move).and_call_original
+
+ service.execute
+
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.git")).to be_falsey
+ expect(gitlab_shell.exists?(project.repository_storage, "#{new_disk_path}.wiki.git")).to be_falsey
+ expect(project.repository_read_only?).to be_falsey
+ end
+
+ context 'when rollback fails' do
+ before do
+ legacy_storage.ensure_storage_path_exists
+ gitlab_shell.mv_repository(project.repository_storage, old_disk_path, new_disk_path)
+ end
+
+ it 'does not try to move nil repository over existing' do
+ expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage, old_disk_path, new_disk_path)
+ expect_move_repository("#{old_disk_path}.wiki", "#{new_disk_path}.wiki")
+
+ service.execute
+ end
+ end
+ end
+
+ it 'works even when project validation fails' do
+ allow(project).to receive(:valid?) { false }
+
+ expect { service.execute }.to change { project.legacy_storage? }.to(true)
+ end
+
+ def expect_move_repository(from_name, to_name)
+ expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage, from_name, to_name).and_call_original
+ end
+ end
+end
diff --git a/spec/services/projects/hashed_storage/rollback_service_spec.rb b/spec/services/projects/hashed_storage/rollback_service_spec.rb
new file mode 100644
index 00000000000..427d1535559
--- /dev/null
+++ b/spec/services/projects/hashed_storage/rollback_service_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::HashedStorage::RollbackService do
+ let(:project) { create(:project, :empty_repo, :wiki_repo) }
+ let(:logger) { double }
+
+ subject(:service) { described_class.new(project, project.full_path, logger: logger) }
+
+ describe '#execute' do
+ context 'attachments rollback' do
+ let(:attachments_service_class) { Projects::HashedStorage::RollbackAttachmentsService }
+ let(:attachments_service) { attachments_service_class.new(project, logger: logger) }
+
+ it 'delegates rollback to Projects::HashedStorage::RollbackAttachmentsService' do
+ expect(attachments_service_class).to receive(:new)
+ .with(project, logger: logger)
+ .and_return(attachments_service)
+ expect(attachments_service).to receive(:execute)
+
+ service.execute
+ end
+
+ it 'does not delegate rollback if repository is in legacy storage already' do
+ project.storage_version = nil
+ expect(attachments_service_class).not_to receive(:new)
+
+ service.execute
+ end
+ end
+
+ context 'repository rollback' do
+ let(:repository_service_class) { Projects::HashedStorage::RollbackRepositoryService }
+ let(:repository_service) { repository_service_class.new(project, project.full_path, logger: logger) }
+
+ it 'delegates rollback to RollbackRepositoryService' do
+ project.storage_version = ::Project::HASHED_STORAGE_FEATURES[:repository]
+
+ expect(repository_service_class).to receive(:new)
+ .with(project, project.full_path, logger: logger)
+ .and_return(repository_service)
+ expect(repository_service).to receive(:execute)
+
+ service.execute
+ end
+
+ it 'does not delegate rollback if repository is in legacy storage already' do
+ project.storage_version = nil
+
+ expect(repository_service_class).not_to receive(:new)
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 18ecef1c0a1..f651db70cbd 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::HousekeepingService do
@@ -79,6 +81,9 @@ describe Projects::HousekeepingService do
# At push 10, 20, ... (except those above)
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid)
.exactly(16).times
+ # At push 6, 12, 18, ... (except those above)
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :pack_refs, :the_lease_key, :the_uuid)
+ .exactly(27).times
201.times do
subject.increment!
@@ -88,6 +93,19 @@ describe Projects::HousekeepingService do
expect(project.pushes_since_gc).to eq(1)
end
end
+
+ it 'runs the task specifically requested' do
+ housekeeping = described_class.new(project, :gc)
+
+ allow(housekeeping).to receive(:try_obtain_lease).and_return(:gc_uuid)
+ allow(housekeeping).to receive(:lease_key).and_return(:gc_lease_key)
+
+ expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :gc_lease_key, :gc_uuid).twice
+
+ 2.times do
+ housekeeping.execute
+ end
+ end
end
describe '#needed?' do
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index f9e5530bc9d..404bb55629a 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ImportExport::ExportService do
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index 7faf0fc2868..d9f9ede8ecd 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -1,17 +1,15 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ImportService do
let!(:project) { create(:project) }
let(:user) { project.creator }
- let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
- let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
subject { described_class.new(project, user) }
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadService).to receive(:execute)
- allow_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
end
describe '#async?' do
@@ -75,7 +73,6 @@ describe Projects::ImportService do
context 'when repository creation succeeds' do
it 'does not download lfs files' do
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -112,7 +109,6 @@ describe Projects::ImportService do
context 'when repository import scheduled' do
it 'does not download lfs objects' do
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -128,7 +124,7 @@ describe Projects::ImportService do
it 'succeeds if repository import is successful' do
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return({})
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :success)
result = subject.execute
@@ -144,6 +140,19 @@ describe Projects::ImportService do
expect(result[:message]).to eq "Error importing repository #{project.safe_import_url} into #{project.full_path} - Failed to import the repository [FILTERED]"
end
+ context 'when lfs import fails' do
+ it 'logs the error' do
+ error_message = 'error message'
+
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
+ expect_any_instance_of(Gitlab::BitbucketImport::Importer).to receive(:execute).and_return(true)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message)
+ expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}")
+
+ subject.execute
+ end
+ end
+
context 'when repository import scheduled' do
before do
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_return(true)
@@ -153,10 +162,7 @@ describe Projects::ImportService do
it 'downloads lfs objects if lfs_enabled is enabled for project' do
allow(project).to receive(:lfs_enabled?).and_return(true)
- service = double
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
- expect(service).to receive(:execute).twice
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute)
subject.execute
end
@@ -164,7 +170,6 @@ describe Projects::ImportService do
it 'does not download lfs objects if lfs_enabled is not enabled for project' do
allow(project).to receive(:lfs_enabled?).and_return(false)
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -206,7 +211,6 @@ describe Projects::ImportService do
allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(true)
expect_any_instance_of(Projects::LfsPointers::LfsImportService).not_to receive(:execute)
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadService).not_to receive(:execute)
subject.execute
end
@@ -214,13 +218,22 @@ describe Projects::ImportService do
it 'does not have a custom repository importer downloads lfs objects' do
allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false)
- service = double
- expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
- expect(service).to receive(:execute).twice
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute)
subject.execute
end
+
+ context 'when lfs import fails' do
+ it 'logs the error' do
+ error_message = 'error message'
+
+ allow(Gitlab::GithubImport::ParallelImporter).to receive(:imports_repository?).and_return(false)
+ expect_any_instance_of(Projects::LfsPointers::LfsImportService).to receive(:execute).and_return(status: :error, message: error_message)
+ expect(Gitlab::AppLogger).to receive(:error).with("The Lfs import process failed. #{error_message}")
+
+ subject.execute
+ end
+ end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
index f222c52199f..80debcd3a7a 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_link_list_service_spec.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadLinkListService do
@@ -32,7 +33,10 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
before do
allow(project).to receive(:lfs_enabled?).and_return(true)
- allow(Gitlab::HTTP).to receive(:post).and_return(objects_response)
+ response = instance_double(HTTParty::Response)
+ allow(response).to receive(:body).and_return(objects_response.to_json)
+ allow(response).to receive(:success?).and_return(true)
+ allow(Gitlab::HTTP).to receive(:post).and_return(response)
end
describe '#execute' do
@@ -83,11 +87,26 @@ describe Projects::LfsPointers::LfsDownloadLinkListService do
end
describe '#get_download_links' do
- it 'raise errorif request fails' do
+ it 'raise error if request fails' do
allow(Gitlab::HTTP).to receive(:post).and_return(Struct.new(:success?, :message).new(false, 'Failed request'))
expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError)
end
+
+ shared_examples 'JSON parse errors' do |body|
+ it 'raises error' do
+ response = instance_double(HTTParty::Response)
+ allow(response).to receive(:body).and_return(body)
+ allow(response).to receive(:success?).and_return(true)
+ allow(Gitlab::HTTP).to receive(:post).and_return(response)
+
+ expect { subject.send(:get_download_links, new_oids) }.to raise_error(described_class::DownloadLinksError)
+ end
+ end
+
+ it_behaves_like 'JSON parse errors', '{'
+ it_behaves_like 'JSON parse errors', '{}'
+ it_behaves_like 'JSON parse errors', '{ foo: 123 }'
end
describe '#parse_response_links' do
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 876beb39801..75d534c59bf 100644
--- a/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_download_service_spec.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
require 'spec_helper'
describe Projects::LfsPointers::LfsDownloadService do
+ include StubRequests
+
let(:project) { create(:project) }
let(:lfs_content) { SecureRandom.random_bytes(10) }
let(:oid) { Digest::SHA256.hexdigest(lfs_content) }
@@ -61,7 +64,7 @@ describe Projects::LfsPointers::LfsDownloadService do
describe '#execute' do
context 'when file download succeeds' do
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
@@ -103,7 +106,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:size) { 1 }
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
end
it_behaves_like 'no lfs object is created'
@@ -117,7 +120,7 @@ describe Projects::LfsPointers::LfsDownloadService do
context 'when downloaded lfs file has a different oid' do
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(body: lfs_content)
allow_any_instance_of(Digest::SHA256).to receive(:hexdigest).and_return('foobar')
end
@@ -135,7 +138,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:lfs_object) { LfsDownloadObject.new(oid: oid, size: size, link: download_link_with_credentials) }
before do
- WebMock.stub_request(:get, download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
+ stub_full_request(download_link).with(headers: { 'Authorization' => 'Basic dXNlcjpwYXNzd29yZA==' }).to_return(body: lfs_content)
end
it 'the request adds authorization headers' do
@@ -148,7 +151,7 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:local_request_setting) { true }
before do
- WebMock.stub_request(:get, download_link).to_return(body: lfs_content)
+ stub_full_request(download_link, ip_address: '192.168.2.120').to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
@@ -172,7 +175,8 @@ describe Projects::LfsPointers::LfsDownloadService do
with_them do
before do
- WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
+ stub_full_request(download_link, ip_address: '192.168.2.120')
+ .to_return(status: 301, headers: { 'Location' => redirect_link })
end
it_behaves_like 'no lfs object is created'
@@ -183,8 +187,8 @@ describe Projects::LfsPointers::LfsDownloadService do
let(:redirect_link) { "http://example.com/"}
before do
- WebMock.stub_request(:get, download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
- WebMock.stub_request(:get, redirect_link).to_return(body: lfs_content)
+ stub_full_request(download_link).to_return(status: 301, headers: { 'Location' => redirect_link })
+ stub_full_request(redirect_link).to_return(body: lfs_content)
end
it_behaves_like 'lfs object is created'
diff --git a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
index 5a75fb38dec..7ca20a6d751 100644
--- a/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_import_service_spec.rb
@@ -1,146 +1,63 @@
+# frozen_string_literal: true
require 'spec_helper'
describe Projects::LfsPointers::LfsImportService do
+ let(:project) { create(:project) }
+ let(:user) { project.creator }
let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
- let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"}
- let(:group) { create(:group, lfs_enabled: true)}
- let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) }
- let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
- let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
- let(:oids) { { 'oid1' => 123, 'oid2' => 125 } }
let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
- let(:all_oids) { existing_lfs_objects.merge(oids) }
- let(:remote_uri) { URI.parse(lfs_endpoint) }
- subject { described_class.new(project) }
+ subject { described_class.new(project, user) }
- before do
- allow(project.repository).to receive(:lfsconfig_for).and_return(nil)
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
- allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids)
- end
-
- describe '#execute' do
- context 'when no lfs pointer is linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
- expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
- end
-
- it 'retrieves all lfs pointers in the project repository' do
- expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute)
-
- subject.execute
- end
-
- it 'links existent lfs objects to the project' do
- expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
-
- subject.execute
- end
-
- it 'retrieves the download links of non existent objects' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
-
- subject.execute
- end
+ context 'when lfs is enabled for the project' do
+ before do
+ allow(project).to receive(:lfs_enabled?).and_return(true)
end
- context 'when some lfs objects are linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
- end
+ it 'downloads lfs objects' do
+ service = double
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return(oid_download_links)
+ expect(Projects::LfsPointers::LfsDownloadService).to receive(:new).and_return(service).twice
+ expect(service).to receive(:execute).twice
- it 'retrieves the download links of non existent objects' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
+ result = subject.execute
- subject.execute
- end
+ expect(result[:status]).to eq :success
end
- context 'when all lfs objects are linked' do
- before do
- allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
- allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
- end
+ context 'when no downloadable lfs object links' do
+ it 'does not call LfsDownloadService' do
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_return({})
+ expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new)
- it 'retrieves no download links' do
- expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
+ result = subject.execute
- expect(subject.execute).to be_empty
+ expect(result[:status]).to eq :success
end
end
- context 'when lfsconfig file exists' do
- before do
- allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")
- end
+ context 'when an exception is raised' do
+ it 'returns error' do
+ error_message = "error message"
+ expect_any_instance_of(Projects::LfsPointers::LfsObjectDownloadListService).to receive(:execute).and_raise(StandardError, error_message)
- context 'when url points to the same import url host' do
- let(:lfs_endpoint) { "#{import_url}/different_endpoint" }
- let(:service) { double }
+ result = subject.execute
- before do
- allow(service).to receive(:execute)
- end
- it 'downloads lfs object using the new endpoint' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service)
-
- subject.execute
- end
-
- context 'when import url has credentials' do
- let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'}
-
- it 'adds the credentials to the new endpoint' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService)
- .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint"))
- .and_return(service)
-
- subject.execute
- end
-
- context 'when url has its own credentials' do
- let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" }
-
- it 'does not add the import url credentials' do
- expect(Projects::LfsPointers::LfsDownloadLinkListService)
- .to receive(:new).with(project, remote_uri: remote_uri)
- .and_return(service)
-
- subject.execute
- end
- end
- end
- end
-
- context 'when url points to a third party service' do
- let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' }
-
- it 'disables lfs from the project' do
- expect(project.lfs_enabled?).to be_truthy
-
- subject.execute
-
- expect(project.lfs_enabled?).to be_falsey
- end
-
- it 'does not download anything' do
- expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute)
-
- subject.execute
- end
+ expect(result[:status]).to eq :error
+ expect(result[:message]).to eq error_message
end
end
end
- describe '#default_endpoint_uri' do
- let(:import_url) { 'http://www.gitlab.com/demo/repo' }
+ context 'when lfs is not enabled for the project' do
+ it 'does not download lfs objects' do
+ allow(project).to receive(:lfs_enabled?).and_return(false)
+ expect(Projects::LfsPointers::LfsObjectDownloadListService).not_to receive(:new)
+ expect(Projects::LfsPointers::LfsDownloadService).not_to receive(:new)
+
+ result = subject.execute
- it 'adds suffix .git if the url does not have it' do
- expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/)
+ expect(result[:status]).to eq :success
end
end
end
diff --git a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
index b7b153655db..849601c4a63 100644
--- a/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
+++ b/spec/services/projects/lfs_pointers/lfs_link_service_spec.rb
@@ -1,3 +1,4 @@
+# frozen_string_literal: true
require 'spec_helper'
describe Projects::LfsPointers::LfsLinkService do
diff --git a/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
new file mode 100644
index 00000000000..9dac29765a2
--- /dev/null
+++ b/spec/services/projects/lfs_pointers/lfs_object_download_list_service_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Projects::LfsPointers::LfsObjectDownloadListService do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo.git' }
+ let(:default_endpoint) { "#{import_url}/info/lfs/objects/batch"}
+ let(:group) { create(:group, lfs_enabled: true)}
+ let!(:project) { create(:project, namespace: group, import_url: import_url, lfs_enabled: true) }
+ let!(:lfs_objects_project) { create_list(:lfs_objects_project, 2, project: project) }
+ let!(:existing_lfs_objects) { LfsObject.pluck(:oid, :size).to_h }
+ let(:oids) { { 'oid1' => 123, 'oid2' => 125 } }
+ let(:oid_download_links) { { 'oid1' => "#{import_url}/gitlab-lfs/objects/oid1", 'oid2' => "#{import_url}/gitlab-lfs/objects/oid2" } }
+ let(:all_oids) { existing_lfs_objects.merge(oids) }
+ let(:remote_uri) { URI.parse(lfs_endpoint) }
+
+ subject { described_class.new(project) }
+
+ before do
+ allow(project.repository).to receive(:lfsconfig_for).and_return(nil)
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ allow_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute).and_return(all_oids)
+ end
+
+ describe '#execute' do
+ context 'when no lfs pointer is linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return([])
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
+ expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: URI.parse(default_endpoint)).and_call_original
+ end
+
+ it 'retrieves all lfs pointers in the project repository' do
+ expect_any_instance_of(Projects::LfsPointers::LfsListService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'links existent lfs objects to the project' do
+ expect_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute)
+
+ subject.execute
+ end
+
+ it 'retrieves the download links of non existent objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(all_oids)
+
+ subject.execute
+ end
+ end
+
+ context 'when some lfs objects are linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(existing_lfs_objects.keys)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).and_return(oid_download_links)
+ end
+
+ it 'retrieves the download links of non existent objects' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with(oids)
+
+ subject.execute
+ end
+ end
+
+ context 'when all lfs objects are linked' do
+ before do
+ allow_any_instance_of(Projects::LfsPointers::LfsLinkService).to receive(:execute).and_return(all_oids.keys)
+ allow_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute)
+ end
+
+ it 'retrieves no download links' do
+ expect_any_instance_of(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:execute).with({}).and_call_original
+
+ expect(subject.execute).to be_empty
+ end
+ end
+
+ context 'when lfsconfig file exists' do
+ before do
+ allow(project.repository).to receive(:lfsconfig_for).and_return("[lfs]\n\turl = #{lfs_endpoint}\n")
+ end
+
+ context 'when url points to the same import url host' do
+ let(:lfs_endpoint) { "#{import_url}/different_endpoint" }
+ let(:service) { double }
+
+ before do
+ allow(service).to receive(:execute)
+ end
+
+ it 'downloads lfs object using the new endpoint' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService).to receive(:new).with(project, remote_uri: remote_uri).and_return(service)
+
+ subject.execute
+ end
+
+ context 'when import url has credentials' do
+ let(:import_url) { 'http://user:password@www.gitlab.com/demo/repo.git'}
+
+ it 'adds the credentials to the new endpoint' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: URI.parse("http://user:password@www.gitlab.com/demo/repo.git/different_endpoint"))
+ .and_return(service)
+
+ subject.execute
+ end
+
+ context 'when url has its own credentials' do
+ let(:lfs_endpoint) { "http://user1:password1@www.gitlab.com/demo/repo.git/different_endpoint" }
+
+ it 'does not add the import url credentials' do
+ expect(Projects::LfsPointers::LfsDownloadLinkListService)
+ .to receive(:new).with(project, remote_uri: remote_uri)
+ .and_return(service)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ context 'when url points to a third party service' do
+ let(:lfs_endpoint) { 'http://third_party_service.com/info/lfs/objects/' }
+
+ it 'disables lfs from the project' do
+ expect(project.lfs_enabled?).to be_truthy
+
+ subject.execute
+
+ expect(project.lfs_enabled?).to be_falsey
+ end
+
+ it 'does not download anything' do
+ expect_any_instance_of(Projects::LfsPointers::LfsListService).not_to receive(:execute)
+
+ subject.execute
+ end
+ end
+ end
+ end
+
+ describe '#default_endpoint_uri' do
+ let(:import_url) { 'http://www.gitlab.com/demo/repo' }
+
+ it 'adds suffix .git if the url does not have it' do
+ expect(subject.send(:default_endpoint_uri).path).to match(/repo.git/)
+ end
+ end
+end
diff --git a/spec/services/projects/move_access_service_spec.rb b/spec/services/projects/move_access_service_spec.rb
index 88d9d93c33b..efa34c84522 100644
--- a/spec/services/projects/move_access_service_spec.rb
+++ b/spec/services/projects/move_access_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveAccessService do
diff --git a/spec/services/projects/move_deploy_keys_projects_service_spec.rb b/spec/services/projects/move_deploy_keys_projects_service_spec.rb
index c548edf39a8..a5d28fb0fbf 100644
--- a/spec/services/projects/move_deploy_keys_projects_service_spec.rb
+++ b/spec/services/projects/move_deploy_keys_projects_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveDeployKeysProjectsService do
diff --git a/spec/services/projects/move_forks_service_spec.rb b/spec/services/projects/move_forks_service_spec.rb
index f4a5a7f9fc2..8f9f048d5ff 100644
--- a/spec/services/projects/move_forks_service_spec.rb
+++ b/spec/services/projects/move_forks_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveForksService do
diff --git a/spec/services/projects/move_lfs_objects_projects_service_spec.rb b/spec/services/projects/move_lfs_objects_projects_service_spec.rb
index 517a24a982a..114509229c5 100644
--- a/spec/services/projects/move_lfs_objects_projects_service_spec.rb
+++ b/spec/services/projects/move_lfs_objects_projects_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveLfsObjectsProjectsService do
diff --git a/spec/services/projects/move_notification_settings_service_spec.rb b/spec/services/projects/move_notification_settings_service_spec.rb
index 24d69eef86a..54d85404bf6 100644
--- a/spec/services/projects/move_notification_settings_service_spec.rb
+++ b/spec/services/projects/move_notification_settings_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveNotificationSettingsService do
diff --git a/spec/services/projects/move_project_authorizations_service_spec.rb b/spec/services/projects/move_project_authorizations_service_spec.rb
index b4408393624..fe3ba31c881 100644
--- a/spec/services/projects/move_project_authorizations_service_spec.rb
+++ b/spec/services/projects/move_project_authorizations_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveProjectAuthorizationsService do
diff --git a/spec/services/projects/move_project_group_links_service_spec.rb b/spec/services/projects/move_project_group_links_service_spec.rb
index 7ca8cf304fe..6140d679929 100644
--- a/spec/services/projects/move_project_group_links_service_spec.rb
+++ b/spec/services/projects/move_project_group_links_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveProjectGroupLinksService do
diff --git a/spec/services/projects/move_project_members_service_spec.rb b/spec/services/projects/move_project_members_service_spec.rb
index c8c0eac1f13..bdd5cd6a87a 100644
--- a/spec/services/projects/move_project_members_service_spec.rb
+++ b/spec/services/projects/move_project_members_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveProjectMembersService do
diff --git a/spec/services/projects/move_users_star_projects_service_spec.rb b/spec/services/projects/move_users_star_projects_service_spec.rb
index e0545c5a21b..cde188f9f5f 100644
--- a/spec/services/projects/move_users_star_projects_service_spec.rb
+++ b/spec/services/projects/move_users_star_projects_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::MoveUsersStarProjectsService do
diff --git a/spec/services/projects/open_issues_count_service_spec.rb b/spec/services/projects/open_issues_count_service_spec.rb
index 562c14a8df8..8efa34765d0 100644
--- a/spec/services/projects/open_issues_count_service_spec.rb
+++ b/spec/services/projects/open_issues_count_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::OpenIssuesCountService do
diff --git a/spec/services/projects/open_merge_requests_count_service_spec.rb b/spec/services/projects/open_merge_requests_count_service_spec.rb
index 9f49b9ec6a2..0d8227f7db5 100644
--- a/spec/services/projects/open_merge_requests_count_service_spec.rb
+++ b/spec/services/projects/open_merge_requests_count_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::OpenMergeRequestsCountService do
diff --git a/spec/services/projects/operations/update_service_spec.rb b/spec/services/projects/operations/update_service_spec.rb
index 6afae3da80c..7e765659b9d 100644
--- a/spec/services/projects/operations/update_service_spec.rb
+++ b/spec/services/projects/operations/update_service_spec.rb
@@ -11,14 +11,70 @@ describe Projects::Operations::UpdateService do
subject { described_class.new(project, user, params) }
describe '#execute' do
+ context 'metrics dashboard setting' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: 'http://gitlab.com'
+ }
+ }
+ end
+
+ context 'without existing metrics dashboard setting' do
+ it 'creates a setting' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting.external_dashboard_url).to eq(
+ 'http://gitlab.com'
+ )
+ end
+ end
+
+ context 'with existing metrics dashboard setting' do
+ before do
+ create(:project_metrics_setting, project: project)
+ end
+
+ it 'updates the settings' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting.external_dashboard_url).to eq(
+ 'http://gitlab.com'
+ )
+ end
+
+ context 'with blank external_dashboard_url in params' do
+ let(:params) do
+ {
+ metrics_setting_attributes: {
+ external_dashboard_url: ''
+ }
+ }
+ end
+
+ it 'destroys the metrics_setting entry in DB' do
+ expect(result[:status]).to eq(:success)
+
+ expect(project.reload.metrics_setting).to be_nil
+ end
+ end
+ end
+ end
+
context 'error tracking' do
context 'with existing error tracking setting' do
let(:params) do
{
error_tracking_setting_attributes: {
enabled: false,
- api_url: 'http://gitlab.com/api/0/projects/org/project',
- token: 'token'
+ api_host: 'http://gitlab.com/',
+ token: 'token',
+ project: {
+ slug: 'project',
+ name: 'Project',
+ organization_slug: 'org',
+ organization_name: 'Org'
+ }
}
}
end
@@ -32,8 +88,30 @@ describe Projects::Operations::UpdateService do
project.reload
expect(project.error_tracking_setting).not_to be_enabled
- expect(project.error_tracking_setting.api_url).to eq('http://gitlab.com/api/0/projects/org/project')
+ expect(project.error_tracking_setting.api_url).to eq(
+ 'http://gitlab.com/api/0/projects/org/project/'
+ )
expect(project.error_tracking_setting.token).to eq('token')
+ expect(project.error_tracking_setting[:project_name]).to eq('Project')
+ expect(project.error_tracking_setting[:organization_name]).to eq('Org')
+ end
+
+ context 'disable error tracking' do
+ before do
+ params[:error_tracking_setting_attributes][:api_host] = ''
+ params[:error_tracking_setting_attributes][:enabled] = false
+ end
+
+ it 'can set api_url to nil' do
+ expect(result[:status]).to eq(:success)
+
+ project.reload
+ expect(project.error_tracking_setting).not_to be_enabled
+ expect(project.error_tracking_setting.api_url).to be_nil
+ expect(project.error_tracking_setting.token).to eq('token')
+ expect(project.error_tracking_setting[:project_name]).to eq('Project')
+ expect(project.error_tracking_setting[:organization_name]).to eq('Org')
+ end
end
end
@@ -42,8 +120,14 @@ describe Projects::Operations::UpdateService do
{
error_tracking_setting_attributes: {
enabled: true,
- api_url: 'http://gitlab.com/api/0/projects/org/project',
- token: 'token'
+ api_host: 'http://gitlab.com/',
+ token: 'token',
+ project: {
+ slug: 'project',
+ name: 'Project',
+ organization_slug: 'org',
+ organization_name: 'Org'
+ }
}
}
end
@@ -52,8 +136,12 @@ describe Projects::Operations::UpdateService do
expect(result[:status]).to eq(:success)
expect(project.error_tracking_setting).to be_enabled
- expect(project.error_tracking_setting.api_url).to eq('http://gitlab.com/api/0/projects/org/project')
+ expect(project.error_tracking_setting.api_url).to eq(
+ 'http://gitlab.com/api/0/projects/org/project/'
+ )
expect(project.error_tracking_setting.token).to eq('token')
+ expect(project.error_tracking_setting[:project_name]).to eq('Project')
+ expect(project.error_tracking_setting[:organization_name]).to eq('Org')
end
end
diff --git a/spec/services/projects/overwrite_project_service_spec.rb b/spec/services/projects/overwrite_project_service_spec.rb
index c7900629f5f..def39ad3789 100644
--- a/spec/services/projects/overwrite_project_service_spec.rb
+++ b/spec/services/projects/overwrite_project_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::OverwriteProjectService do
diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb
index 6040f9100f8..4def83513a4 100644
--- a/spec/services/projects/participants_service_spec.rb
+++ b/spec/services/projects/participants_service_spec.rb
@@ -1,30 +1,59 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::ParticipantsService do
describe '#groups' do
+ set(:user) { create(:user) }
+ set(:project) { create(:project, :public) }
+ let(:service) { described_class.new(project, user) }
+
+ it 'avoids N+1 queries' do
+ group_1 = create(:group)
+ group_1.add_owner(user)
+
+ service.groups # Run general application warmup queries
+ control_count = ActiveRecord::QueryRecorder.new { service.groups }.count
+
+ group_2 = create(:group)
+ group_2.add_owner(user)
+
+ expect { service.groups }.not_to exceed_query_limit(control_count)
+ end
+
+ it 'returns correct user counts for groups' do
+ group_1 = create(:group)
+ group_1.add_owner(user)
+ group_1.add_owner(create(:user))
+
+ group_2 = create(:group)
+ group_2.add_owner(user)
+ create(:group_member, :access_request, group: group_2, user: create(:user))
+
+ expect(service.groups).to contain_exactly(
+ a_hash_including(name: group_1.full_name, count: 2),
+ a_hash_including(name: group_2.full_name, count: 1)
+ )
+ end
+
describe 'avatar_url' do
- let(:project) { create(:project, :public) }
let(:group) { create(:group, avatar: fixture_file_upload('spec/fixtures/dk.png')) }
- let(:user) { create(:user) }
- let!(:group_member) { create(:group_member, group: group, user: user) }
- it 'should return an url for the avatar' do
- participants = described_class.new(project, user)
- groups = participants.groups
+ before do
+ group.add_owner(user)
+ end
- expect(groups.size).to eq 1
- expect(groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png")
+ it 'returns an url for the avatar' do
+ expect(service.groups.size).to eq 1
+ expect(service.groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png")
end
- it 'should return an url for the avatar with relative url' do
+ it 'returns an url for the avatar with relative url' do
stub_config_setting(relative_url_root: '/gitlab')
stub_config_setting(url: Settings.send(:build_gitlab_url))
- participants = described_class.new(project, user)
- groups = participants.groups
-
- expect(groups.size).to eq 1
- expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/-/system/group/avatar/#{group.id}/dk.png")
+ expect(service.groups.size).to eq 1
+ expect(service.groups.first[:avatar_url]).to eq("/gitlab/uploads/-/system/group/avatar/#{group.id}/dk.png")
end
end
end
diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb
index f4c59735c43..f93e5aae82a 100644
--- a/spec/services/projects/propagate_service_template_spec.rb
+++ b/spec/services/projects/propagate_service_template_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::PropagateServiceTemplate do
diff --git a/spec/services/projects/repository_languages_service_spec.rb b/spec/services/projects/repository_languages_service_spec.rb
new file mode 100644
index 00000000000..46c5095327d
--- /dev/null
+++ b/spec/services/projects/repository_languages_service_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::RepositoryLanguagesService do
+ let(:service) { described_class.new(project, project.owner) }
+
+ context 'when detected_repository_languages flag is set' do
+ let(:project) { create(:project) }
+
+ context 'when a project is without detected programming languages' do
+ it 'schedules a worker and returns the empty result' do
+ expect(::DetectRepositoryLanguagesWorker).to receive(:perform_async).with(project.id)
+ expect(service.execute).to eq([])
+ end
+ end
+
+ context 'when a project is with detected programming languages' do
+ let!(:repository_language) { create(:repository_language, project: project) }
+
+ it 'does not schedule a worker and returns the detected languages' do
+ expect(::DetectRepositoryLanguagesWorker).not_to receive(:perform_async).with(project.id)
+
+ languages = service.execute
+
+ expect(languages.size).to eq(1)
+ expect(languages.last.attributes.values).to eq(
+ [project.id, repository_language.programming_language_id, repository_language.share]
+ )
+ end
+
+ it 'sets detected_repository_languages flag' do
+ expect { service.execute }.to change(project, :detected_repository_languages).from(nil).to(true)
+ end
+ end
+ end
+
+ context 'when detected_repository_languages flag is not set' do
+ let!(:repository_language) { create(:repository_language, project: project) }
+ let(:project) { create(:project, detected_repository_languages: true) }
+ let(:languages) { service.execute }
+
+ it 'returns repository languages' do
+ expect(languages.size).to eq(1)
+ expect(languages.last.attributes.values).to eq(
+ [project.id, repository_language.programming_language_id, repository_language.share]
+ )
+ end
+ end
+end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index aae50d5307f..a47c10d991a 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::TransferService do
@@ -71,32 +73,6 @@ describe Projects::TransferService do
shard_name: project.repository_storage
)
end
-
- context 'new group has a kubernetes cluster' do
- let(:group_cluster) { create(:cluster, :group, :provided_by_gcp) }
- let(:group) { group_cluster.group }
-
- let(:token) { 'aaaa' }
- let(:service_account_creator) { double(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService, execute: true) }
- let(:secrets_fetcher) { double(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService, execute: token) }
-
- subject { transfer_project(project, user, group) }
-
- before do
- expect(Clusters::Gcp::Kubernetes::CreateOrUpdateServiceAccountService).to receive(:namespace_creator).and_return(service_account_creator)
- expect(Clusters::Gcp::Kubernetes::FetchKubernetesTokenService).to receive(:new).and_return(secrets_fetcher)
- end
-
- it 'creates kubernetes namespace for the project' do
- subject
-
- expect(project.kubernetes_namespaces.count).to eq(1)
-
- kubernetes_namespace = group_cluster.kubernetes_namespaces.first
- expect(kubernetes_namespace).to be_present
- expect(kubernetes_namespace.project).to eq(project)
- end
- end
end
context 'when transfer fails' do
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 014aab44281..a1175bf7123 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::UnlinkForkService do
diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb
index 7f5ef3129d7..363d3df0f84 100644
--- a/spec/services/projects/update_pages_configuration_service_spec.rb
+++ b/spec/services/projects/update_pages_configuration_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::UpdatePagesConfigurationService do
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 8b70845befe..b597717c347 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe Projects::UpdatePagesService do
@@ -25,59 +27,6 @@ describe Projects::UpdatePagesService do
it { is_expected.not_to match(Gitlab::PathRegex.namespace_format_regex) }
end
- context 'legacy artifacts' do
- before do
- build.update(legacy_artifacts_file: file)
- build.update(legacy_artifacts_metadata: metadata)
- end
-
- describe 'pages artifacts' do
- it "doesn't delete artifacts after deploying" do
- expect(execute).to eq(:success)
-
- expect(build.reload.artifacts?).to eq(true)
- end
- end
-
- it 'succeeds' do
- expect(project.pages_deployed?).to be_falsey
- expect(execute).to eq(:success)
- expect(project.pages_deployed?).to be_truthy
-
- # Check that all expected files are extracted
- %w[index.html zero .hidden/file].each do |filename|
- expect(File.exist?(File.join(project.public_pages_path, filename))).to be_truthy
- end
- end
-
- it 'limits pages size' do
- stub_application_setting(max_pages_size: 1)
- expect(execute).not_to eq(:success)
- end
-
- it 'removes pages after destroy' do
- expect(PagesWorker).to receive(:perform_in)
- expect(project.pages_deployed?).to be_falsey
- expect(execute).to eq(:success)
- expect(project.pages_deployed?).to be_truthy
- project.destroy
- expect(project.pages_deployed?).to be_falsey
- end
-
- it 'fails if sha on branch is not latest' do
- build.update(ref: 'feature')
-
- expect(execute).not_to eq(:success)
- end
-
- it 'fails for empty file fails' do
- build.update(legacy_artifacts_file: empty_file)
-
- expect { execute }
- .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
- end
- end
-
context 'for new artifacts' do
context "for a valid job" do
before do
@@ -205,7 +154,7 @@ describe Projects::UpdatePagesService do
end
it 'fails for invalid archive' do
- build.update(legacy_artifacts_file: invalid_file)
+ create(:ci_job_artifact, :archive, file: invalid_file, job: build)
expect(execute).not_to eq(:success)
end
@@ -216,8 +165,8 @@ describe Projects::UpdatePagesService do
file = fixture_file_upload('spec/fixtures/pages.zip')
metafile = fixture_file_upload('spec/fixtures/pages.zip.meta')
- build.update(legacy_artifacts_file: file)
- build.update(legacy_artifacts_metadata: metafile)
+ create(:ci_job_artifact, :archive, file: file, job: build)
+ create(:ci_job_artifact, :metadata, file: metafile, job: build)
allow(build).to receive(:artifacts_metadata_entry)
.and_return(metadata)
diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb
index c1e5f788146..be2811ab1e7 100644
--- a/spec/services/projects/update_remote_mirror_service_spec.rb
+++ b/spec/services/projects/update_remote_mirror_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::UpdateRemoteMirrorService do
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 90eaea9c872..1dcfb739eb6 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Projects::UpdateService do
+ include ExternalAuthorizationServiceHelpers
include ProjectForksHelper
let(:user) { create(:user) }
@@ -42,6 +45,7 @@ describe Projects::UpdateService do
it 'updates the project to private' do
expect(TodosDestroyer::ProjectPrivateWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, project.id)
+ expect(TodosDestroyer::ConfidentialIssueWorker).to receive(:perform_in).with(Todo::WAIT_FOR_DELETE, nil, project.id)
result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PRIVATE)
@@ -361,6 +365,46 @@ describe Projects::UpdateService do
call_service
end
end
+
+ context 'with external authorization enabled' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not save the project with an error if the service denies access' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'new-label') { false }
+
+ result = update_project(project, user, { external_authorization_classification_label: 'new-label' })
+
+ expect(result[:message]).to be_present
+ expect(result[:status]).to eq(:error)
+ end
+
+ it 'saves the new label if the service allows access' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'new-label') { true }
+
+ result = update_project(project, user, { external_authorization_classification_label: 'new-label' })
+
+ expect(result[:status]).to eq(:success)
+ expect(project.reload.external_authorization_classification_label).to eq('new-label')
+ end
+
+ it 'checks the default label when the classification label was cleared' do
+ expect(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?).with(user, 'default_label') { true }
+
+ update_project(project, user, { external_authorization_classification_label: '' })
+ end
+
+ it 'does not check the label when it does not change' do
+ expect(::Gitlab::ExternalAuthorization)
+ .not_to receive(:access_allowed?)
+
+ update_project(project, user, { name: 'New name' })
+ end
+ end
end
describe '#run_auto_devops_pipeline?' do
@@ -397,6 +441,8 @@ describe Projects::UpdateService do
context 'when auto devops is set to instance setting' do
before do
project.create_auto_devops!(enabled: nil)
+ project.reload
+
allow(project.auto_devops).to receive(:previous_changes).and_return('enabled' => true)
end
diff --git a/spec/services/projects/update_statistics_service_spec.rb b/spec/services/projects/update_statistics_service_spec.rb
new file mode 100644
index 00000000000..8534853fbc7
--- /dev/null
+++ b/spec/services/projects/update_statistics_service_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Projects::UpdateStatisticsService do
+ let(:service) { described_class.new(project, nil, statistics: statistics)}
+ let(:statistics) { %w(repository_size) }
+
+ describe '#execute' do
+ context 'with a non-existing project' do
+ let(:project) { nil }
+
+ it 'does nothing' do
+ expect_any_instance_of(ProjectStatistics).not_to receive(:refresh!)
+
+ service.execute
+ end
+ end
+
+ context 'with an existing project' do
+ let(:project) { create(:project) }
+
+ it 'refreshes the project statistics' do
+ expect_any_instance_of(ProjectStatistics).to receive(:refresh!)
+ .with(only: statistics.map(&:to_sym))
+ .and_call_original
+
+ service.execute
+ end
+ end
+ end
+end
diff --git a/spec/services/prometheus/adapter_service_spec.rb b/spec/services/prometheus/adapter_service_spec.rb
index 505e2935e93..5e972a966eb 100644
--- a/spec/services/prometheus/adapter_service_spec.rb
+++ b/spec/services/prometheus/adapter_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Prometheus::AdapterService do
diff --git a/spec/services/prometheus/proxy_service_spec.rb b/spec/services/prometheus/proxy_service_spec.rb
new file mode 100644
index 00000000000..4bdb20de4c9
--- /dev/null
+++ b/spec/services/prometheus/proxy_service_spec.rb
@@ -0,0 +1,195 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Prometheus::ProxyService do
+ include ReactiveCachingHelpers
+
+ set(:project) { create(:project) }
+ set(:environment) { create(:environment, project: project) }
+
+ describe '#initialize' do
+ let(:params) { ActionController::Parameters.new(query: '1').permit! }
+
+ it 'initializes attributes' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.proxyable).to eq(environment)
+ expect(result.method).to eq('GET')
+ expect(result.path).to eq('query')
+ expect(result.params).to eq('query' => '1')
+ end
+
+ it 'converts ActionController::Parameters into hash' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.params).to be_an_instance_of(Hash)
+ end
+
+ context 'with unknown params' do
+ let(:params) { ActionController::Parameters.new(query: '1', other_param: 'val').permit! }
+
+ it 'filters unknown params' do
+ result = described_class.new(environment, 'GET', 'query', params)
+
+ expect(result.params).to eq('query' => '1')
+ end
+ end
+ end
+
+ describe '#execute' do
+ let(:prometheus_adapter) { instance_double(PrometheusService) }
+ let(:params) { ActionController::Parameters.new(query: '1').permit! }
+
+ subject { described_class.new(environment, 'GET', 'query', params) }
+
+ context 'when prometheus_adapter is nil' do
+ before do
+ allow(environment).to receive(:prometheus_adapter).and_return(nil)
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'No prometheus server found',
+ http_status: :service_unavailable
+ )
+ end
+ end
+
+ context 'when prometheus_adapter cannot query' do
+ before do
+ allow(environment).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(false)
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'No prometheus server found',
+ http_status: :service_unavailable
+ )
+ end
+ end
+
+ context 'cannot proxy' do
+ subject { described_class.new(environment, 'POST', 'garbage', params) }
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ message: 'Proxy support for this API is not available currently',
+ status: :error
+ )
+ end
+ end
+
+ context 'with caching', :use_clean_rails_memory_store_caching do
+ let(:return_value) { { 'http_status' => 200, 'body' => 'body' } }
+
+ let(:opts) do
+ [environment.class.name, environment.id, 'GET', 'query', { 'query' => '1' }]
+ end
+
+ before do
+ allow(environment).to receive(:prometheus_adapter)
+ .and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(true)
+ end
+
+ context 'when value present in cache' do
+ before do
+ stub_reactive_cache(subject, return_value, opts)
+ end
+
+ it 'returns cached value' do
+ result = subject.execute
+
+ expect(result[:http_status]).to eq(return_value[:http_status])
+ expect(result[:body]).to eq(return_value[:body])
+ end
+ end
+
+ context 'when value not present in cache' do
+ it 'returns nil' do
+ expect(ReactiveCachingWorker)
+ .to receive(:perform_async)
+ .with(subject.class, subject.id, *opts)
+
+ result = subject.execute
+
+ expect(result).to eq(nil)
+ end
+ end
+ end
+
+ context 'call prometheus api' do
+ let(:prometheus_client) { instance_double(Gitlab::PrometheusClient) }
+
+ before do
+ synchronous_reactive_cache(subject)
+
+ allow(environment).to receive(:prometheus_adapter)
+ .and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:can_query?).and_return(true)
+ allow(prometheus_adapter).to receive(:prometheus_client_wrapper)
+ .and_return(prometheus_client)
+ end
+
+ context 'connection to prometheus server succeeds' do
+ let(:rest_client_response) { instance_double(RestClient::Response) }
+ let(:prometheus_http_status_code) { 400 }
+
+ let(:response_body) do
+ '{"status":"error","errorType":"bad_data","error":"parse error at char 1: no expression found in input"}'
+ end
+
+ before do
+ allow(prometheus_client).to receive(:proxy).and_return(rest_client_response)
+
+ allow(rest_client_response).to receive(:code)
+ .and_return(prometheus_http_status_code)
+ allow(rest_client_response).to receive(:body).and_return(response_body)
+ end
+
+ it 'returns the http status code and body from prometheus' do
+ expect(subject.execute).to eq(
+ http_status: prometheus_http_status_code,
+ body: response_body,
+ status: :success
+ )
+ end
+ end
+
+ context 'connection to prometheus server fails' do
+ context 'prometheus client raises Gitlab::PrometheusClient::Error' do
+ before do
+ allow(prometheus_client).to receive(:proxy)
+ .and_raise(Gitlab::PrometheusClient::Error, 'Network connection error')
+ end
+
+ it 'returns error' do
+ expect(subject.execute).to eq(
+ status: :error,
+ message: 'Network connection error',
+ http_status: :service_unavailable
+ )
+ end
+ end
+ end
+ end
+ end
+
+ describe '.from_cache' do
+ it 'initializes an instance of ProxyService class' do
+ result = described_class.from_cache(
+ environment.class.name, environment.id, 'GET', 'query', { 'query' => '1' }
+ )
+
+ expect(result).to be_an_instance_of(described_class)
+ expect(result.proxyable).to eq(environment)
+ expect(result.method).to eq('GET')
+ expect(result.path).to eq('query')
+ expect(result.params).to eq('query' => '1')
+ end
+ end
+end
diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb
index 79b744142c6..82d24ec43f6 100644
--- a/spec/services/protected_branches/create_service_spec.rb
+++ b/spec/services/protected_branches/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedBranches::CreateService do
diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb
index 4a391b6c25c..3287eb9a59b 100644
--- a/spec/services/protected_branches/destroy_service_spec.rb
+++ b/spec/services/protected_branches/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedBranches::DestroyService do
diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb
index 3f6f8e09565..7967ff81075 100644
--- a/spec/services/protected_branches/update_service_spec.rb
+++ b/spec/services/protected_branches/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedBranches::UpdateService do
diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb
index b16acf1d36c..e58a539eb6f 100644
--- a/spec/services/protected_tags/create_service_spec.rb
+++ b/spec/services/protected_tags/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedTags::CreateService do
diff --git a/spec/services/protected_tags/destroy_service_spec.rb b/spec/services/protected_tags/destroy_service_spec.rb
index e12f53a2221..52d1d1caa34 100644
--- a/spec/services/protected_tags/destroy_service_spec.rb
+++ b/spec/services/protected_tags/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedTags::DestroyService do
diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb
index 2ece4e3b07b..ca5109aca9c 100644
--- a/spec/services/protected_tags/update_service_spec.rb
+++ b/spec/services/protected_tags/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProtectedTags::UpdateService do
diff --git a/spec/services/push_event_payload_service_spec.rb b/spec/services/push_event_payload_service_spec.rb
index 81956200bff..855b10c0259 100644
--- a/spec/services/push_event_payload_service_spec.rb
+++ b/spec/services/push_event_payload_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PushEventPayloadService do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index 938764f40b0..95a131e8c86 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -10,11 +10,15 @@ describe QuickActions::InterpretService do
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:commit) { create(:commit, project: project) }
let(:inprogress) { create(:label, project: project, title: 'In Progress') }
+ let(:helmchart) { create(:label, project: project, title: 'Helm Chart Registry') }
let(:bug) { create(:label, project: project, title: 'Bug') }
let(:note) { build(:note, commit_id: merge_request.diff_head_sha) }
let(:service) { described_class.new(project, developer) }
before do
+ stub_licensed_features(multiple_issue_assignees: false,
+ multiple_merge_request_assignees: false)
+
project.add_developer(developer)
end
@@ -93,6 +97,26 @@ describe QuickActions::InterpretService do
end
end
+ shared_examples 'multiword label name starting without ~' do
+ it 'fetches label ids and populates add_label_ids if content contains /label' do
+ helmchart # populate the label
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [helmchart.id])
+ end
+ end
+
+ shared_examples 'label name is included in the middle of another label name' do
+ it 'ignores the sublabel when the content contains the includer label name' do
+ helmchart # populate the label
+ create(:label, project: project, title: 'Chart')
+
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(add_label_ids: [helmchart.id])
+ end
+ end
+
shared_examples 'unlabel command' do
it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do
issuable.update!(label_ids: [inprogress.id]) # populate the label
@@ -505,7 +529,7 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
- it_behaves_like 'assign command' do
+ it_behaves_like 'assign command', :quarantine do
let(:content) { "/assign @#{developer.username} @#{developer2.username}" }
let(:issuable) { merge_request }
end
@@ -623,6 +647,26 @@ describe QuickActions::InterpretService do
let(:issuable) { issue }
end
+ it_behaves_like 'multiword label name starting without ~' do
+ let(:content) { %(/label "#{helmchart.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'multiword label name starting without ~' do
+ let(:content) { %(/label "#{helmchart.title}") }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'label name is included in the middle of another label name' do
+ let(:content) { %(/label ~"#{helmchart.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'label name is included in the middle of another label name' do
+ let(:content) { %(/label ~"#{helmchart.title}") }
+ let(:issuable) { merge_request }
+ end
+
it_behaves_like 'unlabel command' do
let(:content) { %(/unlabel ~"#{inprogress.title}") }
let(:issuable) { issue }
@@ -1526,5 +1570,15 @@ describe QuickActions::InterpretService do
end
end
end
+
+ context "#commands_executed_count" do
+ it 'counts commands executed' do
+ content = "/close and \n/assign me and \n/title new title"
+
+ service.execute(content, issue)
+
+ expect(service.commands_executed_count).to eq(3)
+ end
+ end
end
end
diff --git a/spec/services/releases/create_service_spec.rb b/spec/services/releases/create_service_spec.rb
index 612e9f152e7..e26676cdd55 100644
--- a/spec/services/releases/create_service_spec.rb
+++ b/spec/services/releases/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Releases::CreateService do
@@ -19,6 +21,8 @@ describe Releases::CreateService do
shared_examples 'a successful release creation' do
it 'creates a new release' do
result = service.execute
+
+ expect(project.releases.count).to eq(1)
expect(result[:status]).to eq(:success)
expect(result[:tag]).not_to be_nil
expect(result[:release]).not_to be_nil
@@ -69,4 +73,12 @@ describe Releases::CreateService do
end
end
end
+
+ describe '#find_or_build_release' do
+ it 'does not save the built release' do
+ service.find_or_build_release
+
+ expect(project.releases.count).to eq(0)
+ end
+ end
end
diff --git a/spec/services/releases/destroy_service_spec.rb b/spec/services/releases/destroy_service_spec.rb
index dd5b8708f36..f4c901e6585 100644
--- a/spec/services/releases/destroy_service_spec.rb
+++ b/spec/services/releases/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Releases::DestroyService do
@@ -28,13 +30,11 @@ describe Releases::DestroyService do
end
end
- context 'when tag is not found' do
+ context 'when tag does not exist in the repository' do
let(:tag) { 'v1.1.1' }
- it 'returns an error' do
- is_expected.to include(status: :error,
- message: 'Tag does not exist',
- http_status: 404)
+ it 'removes the orphaned release' do
+ expect { subject }.to change { project.releases.count }.by(-1)
end
end
diff --git a/spec/services/releases/update_service_spec.rb b/spec/services/releases/update_service_spec.rb
index 6c68f364739..14e6a5f13c8 100644
--- a/spec/services/releases/update_service_spec.rb
+++ b/spec/services/releases/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Releases::UpdateService do
diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb
index bf79cfe74b7..9918bb8e054 100644
--- a/spec/services/repair_ldap_blocked_user_service_spec.rb
+++ b/spec/services/repair_ldap_blocked_user_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepairLdapBlockedUserService do
diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb
index ab1c638fc39..60a14d7a107 100644
--- a/spec/services/repository_archive_clean_up_service_spec.rb
+++ b/spec/services/repository_archive_clean_up_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryArchiveCleanUpService do
diff --git a/spec/services/reset_project_cache_service_spec.rb b/spec/services/reset_project_cache_service_spec.rb
index 1490ad5fe3b..a4db4481c36 100644
--- a/spec/services/reset_project_cache_service_spec.rb
+++ b/spec/services/reset_project_cache_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ResetProjectCacheService do
diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb
index 980545b8083..62a70ccd3da 100644
--- a/spec/services/search/global_service_spec.rb
+++ b/spec/services/search/global_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Search::GlobalService do
diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb
index cbc553a60cf..aac2f3fe4cb 100644
--- a/spec/services/search/group_service_spec.rb
+++ b/spec/services/search/group_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Search::GroupService do
diff --git a/spec/services/search/snippet_service_spec.rb b/spec/services/search/snippet_service_spec.rb
index 8ad162ad66e..430c71880a3 100644
--- a/spec/services/search/snippet_service_spec.rb
+++ b/spec/services/search/snippet_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Search::SnippetService do
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index e5e036c7d44..48065bf596a 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SearchService do
diff --git a/spec/services/service_response_spec.rb b/spec/services/service_response_spec.rb
new file mode 100644
index 00000000000..e790d272e61
--- /dev/null
+++ b/spec/services/service_response_spec.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+
+ActiveSupport::Dependencies.autoload_paths << 'app/services'
+
+describe ServiceResponse do
+ describe '.success' do
+ it 'creates a successful response without a message' do
+ expect(described_class.success).to be_success
+ end
+
+ it 'creates a successful response with a message' do
+ response = described_class.success(message: 'Good orange')
+
+ expect(response).to be_success
+ expect(response.message).to eq('Good orange')
+ end
+
+ it 'creates a successful response with payload' do
+ response = described_class.success(payload: { good: 'orange' })
+
+ expect(response).to be_success
+ expect(response.payload).to eq(good: 'orange')
+ end
+ end
+
+ describe '.error' do
+ it 'creates a failed response without HTTP status' do
+ response = described_class.error(message: 'Bad apple')
+
+ expect(response).to be_error
+ expect(response.message).to eq('Bad apple')
+ end
+
+ it 'creates a failed response with HTTP status' do
+ response = described_class.error(message: 'Bad apple', http_status: 400)
+
+ expect(response).to be_error
+ expect(response.message).to eq('Bad apple')
+ expect(response.http_status).to eq(400)
+ end
+
+ it 'creates a failed response with payload' do
+ response = described_class.error(message: 'Bad apple',
+ payload: { bad: 'apple' })
+
+ expect(response).to be_error
+ expect(response.message).to eq('Bad apple')
+ expect(response.payload).to eq(bad: 'apple')
+ end
+ end
+
+ describe '#success?' do
+ it 'returns true for a successful response' do
+ expect(described_class.success.success?).to eq(true)
+ end
+
+ it 'returns false for a failed response' do
+ expect(described_class.error(message: 'Bad apple').success?).to eq(false)
+ end
+ end
+
+ describe '#error?' do
+ it 'returns false for a successful response' do
+ expect(described_class.success.error?).to eq(false)
+ end
+
+ it 'returns true for a failed response' do
+ expect(described_class.error(message: 'Bad apple').error?).to eq(true)
+ end
+ end
+end
diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb
index 61312d55b84..b9e5e844c1f 100644
--- a/spec/services/spam_service_spec.rb
+++ b/spec/services/spam_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SpamService do
diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb
index c8a6fc1a99b..653f17a4324 100644
--- a/spec/services/submit_usage_ping_service_spec.rb
+++ b/spec/services/submit_usage_ping_service_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SubmitUsagePingService do
+ include StubRequests
+
context 'when usage ping is disabled' do
before do
stub_application_setting(usage_ping_enabled: false)
@@ -97,7 +101,7 @@ describe SubmitUsagePingService do
end
def stub_response(body)
- stub_request(:post, 'https://version.gitlab.com/usage_data')
+ stub_full_request('https://version.gitlab.com/usage_data', method: :post)
.to_return(
headers: { 'Content-Type' => 'application/json' },
body: body.to_json
diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb
index 8e77d582eb4..bdbcb0fdb07 100644
--- a/spec/services/suggestions/apply_service_spec.rb
+++ b/spec/services/suggestions/apply_service_spec.rb
@@ -5,21 +5,63 @@ require 'spec_helper'
describe Suggestions::ApplyService do
include ProjectForksHelper
+ def build_position(args = {})
+ default_args = { old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 9,
+ diff_refs: merge_request.diff_refs }
+
+ Gitlab::Diff::Position.new(default_args.merge(args))
+ end
+
+ shared_examples 'successfully creates commit and updates suggestion' do
+ def apply(suggestion)
+ result = subject.execute(suggestion)
+ expect(result[:status]).to eq(:success)
+ end
+
+ it 'updates the file with the new contents' do
+ apply(suggestion)
+
+ blob = project.repository.blob_at_branch(merge_request.source_branch,
+ position.new_path)
+
+ expect(blob.data).to eq(expected_content)
+ end
+
+ it 'updates suggestion applied and commit_id columns' do
+ expect { apply(suggestion) }
+ .to change(suggestion, :applied)
+ .from(false).to(true)
+ .and change(suggestion, :commit_id)
+ .from(nil)
+ end
+
+ it 'created commit has users email and name' do
+ apply(suggestion)
+
+ commit = project.repository.commit
+
+ expect(user.commit_email).not_to eq(user.email)
+ expect(commit.author_email).to eq(user.commit_email)
+ expect(commit.committer_email).to eq(user.commit_email)
+ expect(commit.author_name).to eq(user.name)
+ end
+ end
+
let(:project) { create(:project, :repository) }
let(:user) { create(:user, :commit_email) }
- let(:position) do
- Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 9,
- diff_refs: merge_request.diff_refs)
+ let(:position) { build_position }
+
+ let(:diff_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, position: position, project: project)
end
let(:suggestion) do
- create(:suggestion, note: diff_note,
- from_content: " raise RuntimeError, \"System commands must be given as an array of strings\"\n",
- to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n")
+ create(:suggestion, :content_from_repo, note: diff_note,
+ to_content: " raise RuntimeError, 'Explosion'\n # explosion?\n")
end
subject { described_class.new(user) }
@@ -74,49 +116,11 @@ describe Suggestions::ApplyService do
target_project: project)
end
- let!(:diff_note) do
- create(:diff_note_on_merge_request, noteable: merge_request,
- position: position,
- project: project)
- end
-
before do
project.add_maintainer(user)
end
- it 'updates the file with the new contents' do
- subject.execute(suggestion)
-
- blob = project.repository.blob_at_branch(merge_request.source_branch,
- position.new_path)
-
- expect(blob.data).to eq(expected_content)
- end
-
- it 'returns success status' do
- result = subject.execute(suggestion)
-
- expect(result[:status]).to eq(:success)
- end
-
- it 'updates suggestion applied and commit_id columns' do
- expect { subject.execute(suggestion) }
- .to change(suggestion, :applied)
- .from(false).to(true)
- .and change(suggestion, :commit_id)
- .from(nil)
- end
-
- it 'created commit has users email and name' do
- subject.execute(suggestion)
-
- commit = project.repository.commit
-
- expect(user.commit_email).not_to eq(user.email)
- expect(commit.author_email).to eq(user.commit_email)
- expect(commit.committer_email).to eq(user.commit_email)
- expect(commit.author_name).to eq(user.name)
- end
+ it_behaves_like 'successfully creates commit and updates suggestion'
context 'when it fails to apply because the file was changed' do
it 'returns error message' do
@@ -190,11 +194,6 @@ describe Suggestions::ApplyService do
CONTENT
end
- let(:merge_request) do
- create(:merge_request, source_project: project,
- target_project: project)
- end
-
def create_suggestion(diff, old_line: nil, new_line: nil, from_content:, to_content:, path:)
position = Gitlab::Diff::Position.new(old_path: path,
new_path: path,
@@ -212,11 +211,13 @@ describe Suggestions::ApplyService do
end
def apply_suggestion(suggestion)
- suggestion.note.reload
+ suggestion.reload
merge_request.reload
merge_request.clear_memoized_shas
result = subject.execute(suggestion)
+ expect(result[:status]).to eq(:success)
+
refresh = MergeRequests::RefreshService.new(project, user)
refresh.execute(merge_request.diff_head_sha,
suggestion.commit_id,
@@ -241,7 +242,7 @@ describe Suggestions::ApplyService do
suggestion_2_changes = { old_line: 24,
new_line: 31,
- from_content: " @cmd_output << stderr.read\n",
+ from_content: " @cmd_output << stderr.read\n",
to_content: "# v2 change\n",
path: path }
@@ -287,6 +288,105 @@ describe Suggestions::ApplyService do
expect(suggestion_2_diff.strip).to eq(expected_suggestion_2_diff.strip)
end
end
+
+ context 'multi-line suggestion' do
+ let(:expected_content) do
+ <<~CONTENT
+ require 'fileutils'
+ require 'open3'
+
+ module Popen
+ extend self
+
+ # multi
+ # line
+
+ vars = {
+ "PWD" => path
+ }
+
+ options = {
+ chdir: path
+ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+ end
+
+ @cmd_output = ""
+ @cmd_status = 0
+
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ return @cmd_output, @cmd_status
+ end
+ end
+ CONTENT
+ end
+
+ let(:suggestion) do
+ create(:suggestion, :content_from_repo, note: diff_note,
+ lines_above: 2,
+ lines_below: 3,
+ to_content: "# multi\n# line\n")
+ end
+
+ it_behaves_like 'successfully creates commit and updates suggestion'
+ end
+
+ context 'remove an empty line suggestion' do
+ let(:expected_content) do
+ <<~CONTENT
+ require 'fileutils'
+ require 'open3'
+
+ module Popen
+ extend self
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+
+ path ||= Dir.pwd
+ vars = {
+ "PWD" => path
+ }
+
+ options = {
+ chdir: path
+ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+ end
+
+ @cmd_output = ""
+ @cmd_status = 0
+
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+ @cmd_status = wait_thr.value.exitstatus
+ end
+
+ return @cmd_output, @cmd_status
+ end
+ end
+ CONTENT
+ end
+
+ let(:position) { build_position(new_line: 13) }
+ let(:suggestion) do
+ create(:suggestion, :content_from_repo, note: diff_note, to_content: "")
+ end
+
+ it_behaves_like 'successfully creates commit and updates suggestion'
+ end
end
context 'fork-project' do
@@ -362,6 +462,28 @@ describe Suggestions::ApplyService do
project.add_maintainer(user)
end
+ context 'diff file was not found' do
+ it 'returns error message' do
+ expect(suggestion.note).to receive(:latest_diff_file) { nil }
+
+ result = subject.execute(suggestion)
+
+ expect(result).to eq(message: 'Suggestion is not appliable',
+ status: :error)
+ end
+ end
+
+ context 'suggestion is eligible to be outdated' do
+ it 'returns error message' do
+ expect(suggestion).to receive(:outdated?) { true }
+
+ result = subject.execute(suggestion)
+
+ expect(result).to eq(message: 'Suggestion is not appliable',
+ status: :error)
+ end
+ end
+
context 'suggestion was already applied' do
it 'returns success status' do
result = subject.execute(suggestion)
diff --git a/spec/services/suggestions/create_service_spec.rb b/spec/services/suggestions/create_service_spec.rb
index f1142c88a69..d95f9e3349b 100644
--- a/spec/services/suggestions/create_service_spec.rb
+++ b/spec/services/suggestions/create_service_spec.rb
@@ -9,14 +9,18 @@ describe Suggestions::CreateService do
target_project: project_with_repo)
end
- let(:position) do
- Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
- new_path: "files/ruby/popen.rb",
- old_line: nil,
- new_line: 14,
- diff_refs: merge_request.diff_refs)
+ def build_position(args = {})
+ default_args = { old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: 14,
+ diff_refs: merge_request.diff_refs }
+
+ Gitlab::Diff::Position.new(default_args.merge(args))
end
+ let(:position) { build_position }
+
let(:markdown) do
<<-MARKDOWN.strip_heredoc
```suggestion
@@ -36,6 +40,14 @@ describe Suggestions::CreateService do
```thing
this is not a suggestion, it's a thing
```
+
+ ```suggestion:-3+2
+ # multi-line suggestion 1
+ ```
+
+ ```suggestion:-5
+ # multi-line suggestion 1
+ ```
MARKDOWN
end
@@ -50,7 +62,7 @@ describe Suggestions::CreateService do
end
it 'does not try to parse suggestions' do
- expect(Banzai::SuggestionsParser).not_to receive(:parse)
+ expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse)
subject.execute
end
@@ -67,13 +79,30 @@ describe Suggestions::CreateService do
it 'does not try to parse suggestions' do
allow(note).to receive(:on_text?) { false }
- expect(Banzai::SuggestionsParser).not_to receive(:parse)
+ expect(Gitlab::Diff::SuggestionsParser).not_to receive(:parse)
subject.execute
end
end
end
+ context 'should not create suggestions' do
+ let(:note) do
+ create(:diff_note_on_merge_request, project: project_with_repo,
+ noteable: merge_request,
+ position: position,
+ note: markdown)
+ end
+
+ it 'creates no suggestion when diff file is not found' do
+ expect_next_instance_of(DiffNote) do |diff_note|
+ expect(diff_note).to receive(:latest_diff_file).once { nil }
+ end
+
+ expect { subject.execute }.not_to change(Suggestion, :count)
+ end
+ end
+
context 'should create suggestions' do
let(:note) do
create(:diff_note_on_merge_request, project: project_with_repo,
@@ -82,27 +111,64 @@ describe Suggestions::CreateService do
note: markdown)
end
- context 'single line suggestions' do
- it 'persists suggestion records' do
- expect { subject.execute }
- .to change { note.suggestions.count }
- .from(0)
- .to(2)
+ let(:expected_suggestions) do
+ Gitlab::Diff::SuggestionsParser.parse(markdown,
+ project: note.project,
+ position: note.position)
+ end
+
+ it 'persists suggestion records' do
+ expect { subject.execute }.to change { note.suggestions.count }
+ .from(0).to(expected_suggestions.size)
+ end
+
+ it 'persists suggestions data correctly' do
+ subject.execute
+
+ suggestions = note.suggestions.order(:relative_order)
+
+ suggestions.zip(expected_suggestions) do |suggestion, expected_suggestion|
+ expected_data = expected_suggestion.to_hash
+
+ expect(suggestion.from_content).to eq(expected_data[:from_content])
+ expect(suggestion.to_content).to eq(expected_data[:to_content])
+ expect(suggestion.lines_above).to eq(expected_data[:lines_above])
+ expect(suggestion.lines_below).to eq(expected_data[:lines_below])
end
+ end
+
+ context 'outdated position note' do
+ let!(:outdated_diff) { merge_request.merge_request_diff }
+ let!(:latest_diff) { merge_request.create_merge_request_diff }
+ let(:outdated_position) { build_position(diff_refs: outdated_diff.diff_refs) }
+ let(:position) { build_position(diff_refs: latest_diff.diff_refs) }
+
+ it 'uses the correct position when creating the suggestion' do
+ expect(Gitlab::Diff::SuggestionsParser).to receive(:parse)
+ .with(note.note, project: note.project, position: note.position)
+ .and_call_original
- it 'persists original from_content lines and suggested lines' do
subject.execute
+ end
+ end
- suggestions = note.suggestions.order(:relative_order)
+ context 'when a patch removes an empty line' do
+ let(:markdown) do
+ <<-MARKDOWN.strip_heredoc
+ ```suggestion
+ ```
+ MARKDOWN
+ end
+ let(:position) { build_position(new_line: 13) }
- suggestion_1 = suggestions.first
- suggestion_2 = suggestions.last
+ it 'creates an appliable suggestion' do
+ subject.execute
- expect(suggestion_1).to have_attributes(from_content: " vars = {\n",
- to_content: " foo\n bar\n")
+ suggestion = note.suggestions.last
- expect(suggestion_2).to have_attributes(from_content: " vars = {\n",
- to_content: " xpto\n baz\n")
+ expect(suggestion).to be_appliable
+ expect(suggestion.from_content).to eq("\n")
+ expect(suggestion.to_content).to eq("")
end
end
end
diff --git a/spec/services/suggestions/outdate_service_spec.rb b/spec/services/suggestions/outdate_service_spec.rb
new file mode 100644
index 00000000000..bcc627013d8
--- /dev/null
+++ b/spec/services/suggestions/outdate_service_spec.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Suggestions::OutdateService do
+ describe '#execute' do
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.target_project }
+ let(:user) { merge_request.author }
+ let(:file_path) { 'files/ruby/popen.rb' }
+ let(:branch_name) { project.default_branch }
+ let(:diff_file) { suggestion.diff_file }
+ let(:position) { build_position(file_path, comment_line) }
+ let(:note) do
+ create(:diff_note_on_merge_request, noteable: merge_request,
+ position: position,
+ project: project)
+ end
+
+ def build_position(path, line)
+ Gitlab::Diff::Position.new(old_path: path,
+ new_path: path,
+ old_line: nil,
+ new_line: line,
+ diff_refs: merge_request.diff_refs)
+ end
+
+ def commit_changes(file_path, new_content)
+ params = {
+ file_path: file_path,
+ commit_message: "Update File",
+ file_content: new_content,
+ start_project: project,
+ start_branch: project.default_branch,
+ branch_name: branch_name
+ }
+
+ Files::UpdateService.new(project, user, params).execute
+ end
+
+ def update_file_line(diff_file, change_line, content)
+ new_lines = diff_file.new_blob.data.lines
+ new_lines[change_line..change_line] = content
+ result = commit_changes(diff_file.file_path, new_lines.join)
+ newrev = result[:result]
+
+ expect(result[:status]).to eq(:success)
+ expect(newrev).to be_present
+
+ # Ensure all memoized data is cleared in order
+ # to generate the new merge_request_diff.
+ MergeRequest.find(merge_request.id).reload_diff(user)
+
+ note.reload
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ subject { described_class.new.execute(merge_request) }
+
+ context 'when there is a change within multi-line suggestion range' do
+ let(:comment_line) { 9 }
+ let(:lines_above) { 8 } # suggesting to change lines 1..9
+ let(:change_line) { 2 } # line 2 is within the range
+ let!(:suggestion) do
+ create(:suggestion, :content_from_repo, note: note, lines_above: lines_above)
+ end
+
+ it 'updates the outdatable suggestion record' do
+ update_file_line(diff_file, change_line, "# foo\nbar\n")
+
+ # Make sure note is still active
+ expect(note.active?).to be(true)
+
+ expect { subject }.to change { suggestion.reload.outdated }
+ .from(false).to(true)
+ end
+ end
+
+ context 'when there is no change within multi-line suggestion range' do
+ let(:comment_line) { 9 }
+ let(:lines_above) { 3 } # suggesting to change lines 6..9
+ let(:change_line) { 2 } # line 2 is not within the range
+ let!(:suggestion) do
+ create(:suggestion, :content_from_repo, note: note, lines_above: lines_above)
+ end
+
+ subject { described_class.new.execute(merge_request) }
+
+ it 'does not outdates suggestion record' do
+ update_file_line(diff_file, change_line, "# foo\nbar\n")
+
+ # Make sure note is still active
+ expect(note.active?).to be(true)
+
+ expect { subject }.not_to change { suggestion.reload.outdated }.from(false)
+ end
+ end
+ end
+end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index 81b2c17fdb5..f5c6e972953 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SystemHooksService do
@@ -84,20 +86,20 @@ describe SystemHooksService do
context 'group_rename' do
it 'contains old and new path' do
- allow(group).to receive(:path_was).and_return('old-path')
+ allow(group).to receive(:path_before_last_save).and_return('old-path')
data = event_data(group, :rename)
expect(data).to include(:event_name, :name, :created_at, :updated_at, :full_path, :path, :group_id, :old_path, :old_full_path)
expect(data[:path]).to eq(group.path)
expect(data[:full_path]).to eq(group.path)
- expect(data[:old_path]).to eq(group.path_was)
- expect(data[:old_full_path]).to eq(group.path_was)
+ expect(data[:old_path]).to eq(group.path_before_last_save)
+ expect(data[:old_full_path]).to eq(group.path_before_last_save)
end
it 'contains old and new full_path for subgroup' do
subgroup = create(:group, parent: group)
- allow(subgroup).to receive(:path_was).and_return('old-path')
+ allow(subgroup).to receive(:path_before_last_save).and_return('old-path')
data = event_data(subgroup, :rename)
@@ -108,13 +110,13 @@ describe SystemHooksService do
context 'user_rename' do
it 'contains old and new username' do
- allow(user).to receive(:username_was).and_return('old-username')
+ allow(user).to receive(:username_before_last_save).and_return('old-username')
data = event_data(user, :rename)
expect(data).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username, :old_username)
expect(data[:username]).to eq(user.username)
- expect(data[:old_username]).to eq(user.username_was)
+ expect(data[:old_username]).to eq(user.username_before_last_save)
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 82544ab0413..2420817e1f7 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1,6 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SystemNoteService do
+ include ProjectForksHelper
include Gitlab::Routing
include RepoHelpers
include AssetsHelpers
@@ -11,6 +14,14 @@ describe SystemNoteService do
let(:noteable) { create(:issue, project: project) }
let(:issue) { noteable }
+ shared_examples_for 'a note with overridable created_at' do
+ let(:noteable) { create(:issue, project: project, system_note_timestamp: Time.at(42)) }
+
+ it 'the note has the correct time' do
+ expect(subject.created_at).to eq Time.at(42)
+ end
+ end
+
shared_examples_for 'a system note' do
let(:expected_noteable) { noteable }
let(:commit_count) { nil }
@@ -121,7 +132,7 @@ describe SystemNoteService do
end
it 'sets the note text' do
- link = "http://localhost/#{project.full_path}/tags/#{tag_name}"
+ link = "/#{project.full_path}/-/tags/#{tag_name}"
expect(subject.note).to eq "tagged commit #{noteable.sha} to [`#{tag_name}`](#{link})"
end
@@ -137,6 +148,8 @@ describe SystemNoteService do
end
context 'when assignee added' do
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq "assigned to @#{assignee.username}"
end
@@ -145,14 +158,16 @@ describe SystemNoteService do
context 'when assignee removed' do
let(:assignee) { nil }
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq 'removed assignee'
end
end
end
- describe '.change_issue_assignees' do
- subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) }
+ describe '.change_issuable_assignees' do
+ subject { described_class.change_issuable_assignees(noteable, project, author, [assignee]) }
let(:assignee) { create(:user) }
let(:assignee1) { create(:user) }
@@ -165,9 +180,11 @@ describe SystemNoteService do
def build_note(old_assignees, new_assignees)
issue.assignees = new_assignees
- described_class.change_issue_assignees(issue, project, author, old_assignees).note
+ described_class.change_issuable_assignees(issue, project, author, old_assignees).note
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'builds a correct phrase when an assignee is added to a non-assigned issue' do
expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}"
end
@@ -213,6 +230,8 @@ describe SystemNoteService do
expect(subject.note).to eq "changed milestone to #{reference}"
end
+
+ it_behaves_like 'a note with overridable created_at'
end
context 'when milestone removed' do
@@ -221,6 +240,8 @@ describe SystemNoteService do
it 'sets the note text' do
expect(subject.note).to eq 'removed milestone'
end
+
+ it_behaves_like 'a note with overridable created_at'
end
end
@@ -237,6 +258,8 @@ describe SystemNoteService do
it 'sets the note text to use the milestone name' do
expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}"
end
+
+ it_behaves_like 'a note with overridable created_at'
end
context 'when milestone removed' do
@@ -245,6 +268,8 @@ describe SystemNoteService do
it 'sets the note text' do
expect(subject.note).to eq 'removed milestone'
end
+
+ it_behaves_like 'a note with overridable created_at'
end
end
end
@@ -254,6 +279,8 @@ describe SystemNoteService do
let(:due_date) { Date.today }
+ it_behaves_like 'a note with overridable created_at'
+
it_behaves_like 'a system note' do
let(:action) { 'due_date' }
end
@@ -280,6 +307,8 @@ describe SystemNoteService do
let(:status) { 'reopened' }
let(:source) { nil }
+ it_behaves_like 'a note with overridable created_at'
+
it_behaves_like 'a system note' do
let(:action) { 'opened' }
end
@@ -289,6 +318,8 @@ describe SystemNoteService do
let(:status) { 'opened' }
let(:source) { double('commit', gfm_reference: 'commit 123456') }
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq "#{status} via commit 123456"
end
@@ -338,6 +369,8 @@ describe SystemNoteService do
let(:action) { 'title' }
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note)
.to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**"
@@ -353,6 +386,8 @@ describe SystemNoteService do
let(:action) { 'description' }
end
+ it_behaves_like 'a note with overridable created_at'
+
it 'sets the note text' do
expect(subject.note).to eq('changed the description')
end
@@ -478,6 +513,8 @@ describe SystemNoteService do
let(:action) { 'cross_reference' }
end
+ it_behaves_like 'a note with overridable created_at'
+
describe 'note_body' do
context 'cross-project' do
let(:project2) { create(:project, :repository) }
@@ -619,7 +656,7 @@ describe SystemNoteService do
context 'commit with cross-reference from fork' do
let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
- let(:forked_project) { Projects::ForkService.new(project, author2).execute }
+ let(:forked_project) { fork_project(project, author2, repository: true) }
let(:commit2) { forked_project.commit }
before do
@@ -807,9 +844,10 @@ describe SystemNoteService do
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: "GitLab: Mentioned on commit - #{commit.title}",
+ title: "Commit - #{commit.title}",
icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
@@ -833,9 +871,10 @@ describe SystemNoteService do
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: "GitLab: Mentioned on issue - #{issue.title}",
+ title: "Issue - #{issue.title}",
icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
@@ -859,9 +898,10 @@ describe SystemNoteService do
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: "GitLab: Mentioned on snippet - #{snippet.title}",
+ title: "Snippet - #{snippet.title}",
icon: { title: "GitLab", url16x16: favicon_path },
status: { resolved: false }
}
@@ -893,6 +933,28 @@ describe SystemNoteService do
end
end
+ describe '.change_time_estimate' do
+ subject { described_class.change_time_estimate(noteable, project, author) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'time_tracking' }
+ end
+
+ context 'with a time estimate' do
+ it 'sets the note text' do
+ noteable.update_attribute(:time_estimate, 277200)
+
+ expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
+ end
+ end
+
+ context 'without a time estimate' do
+ it 'sets the note text' do
+ expect(subject.note).to eq "removed time estimate"
+ end
+ end
+ end
+
describe '.discussion_continued_in_issue' do
let(:discussion) { create(:diff_note_on_merge_request, project: project).to_discussion }
let(:merge_request) { discussion.noteable }
@@ -919,28 +981,6 @@ describe SystemNoteService do
end
end
- describe '.change_time_estimate' do
- subject { described_class.change_time_estimate(noteable, project, author) }
-
- it_behaves_like 'a system note' do
- let(:action) { 'time_tracking' }
- end
-
- context 'with a time estimate' do
- it 'sets the note text' do
- noteable.update_attribute(:time_estimate, 277200)
-
- expect(subject.note).to eq "changed time estimate to 1w 4d 5h"
- end
- end
-
- context 'without a time estimate' do
- it 'sets the note text' do
- expect(subject.note).to eq "removed time estimate"
- end
- end
- end
-
describe '.change_time_spent' do
# We need a custom noteable in order to the shared examples to be green.
let(:noteable) do
@@ -1099,7 +1139,7 @@ describe SystemNoteService do
diff_id = merge_request.merge_request_diff.id
line_code = change_position.line_code(project.repository)
- expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code))
+ expect(subject.note).to include(diffs_project_merge_request_path(project, merge_request, diff_id: diff_id, anchor: line_code))
end
end
diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb
index 0cbe57352be..ee558f90d6f 100644
--- a/spec/services/tags/create_service_spec.rb
+++ b/spec/services/tags/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Tags::CreateService do
@@ -41,7 +43,7 @@ describe Tags::CreateService do
it 'returns an error' do
expect(repository).to receive(:add_tag)
.with(user, 'v1.1.0', 'master', 'Foo')
- .and_raise(Gitlab::Git::PreReceiveError, 'something went wrong')
+ .and_raise(Gitlab::Git::PreReceiveError, 'GitLab: something went wrong')
response = service.execute('v1.1.0', 'master', 'Foo')
diff --git a/spec/services/tags/destroy_service_spec.rb b/spec/services/tags/destroy_service_spec.rb
index 7c8c1dd0d3a..b46bd77eafe 100644
--- a/spec/services/tags/destroy_service_spec.rb
+++ b/spec/services/tags/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Tags::DestroyService do
@@ -7,11 +9,27 @@ describe Tags::DestroyService do
let(:service) { described_class.new(project, user) }
describe '#execute' do
+ subject { service.execute(tag_name) }
+
it 'removes the tag' do
expect(repository).to receive(:before_remove_tag)
expect(service).to receive(:success)
service.execute('v1.1.0')
end
+
+ context 'when there is an associated release on the tag' do
+ let(:tag) { repository.tags.first }
+ let(:tag_name) { tag.name }
+
+ before do
+ project.add_maintainer(user)
+ create(:release, tag: tag_name, project: project)
+ end
+
+ it 'destroys the release' do
+ expect { subject }.to change { project.releases.count }.by(-1)
+ end
+ end
end
end
diff --git a/spec/services/task_list_toggle_service_spec.rb b/spec/services/task_list_toggle_service_spec.rb
index b1260cf740a..9adaee6481b 100644
--- a/spec/services/task_list_toggle_service_spec.rb
+++ b/spec/services/task_list_toggle_service_spec.rb
@@ -113,4 +113,25 @@ describe TaskListToggleService do
expect(toggler.execute).to be_falsey
end
+
+ it 'properly handles a GitLab blockquote' do
+ markdown =
+ <<-EOT.strip_heredoc
+ >>>
+ gitlab blockquote
+ >>>
+
+ * [ ] Task 1
+ * [x] Task 2
+ EOT
+
+ markdown_html = Banzai::Pipeline::FullPipeline.call(markdown, project: nil)[:output].to_html
+ toggler = described_class.new(markdown, markdown_html,
+ toggle_as_checked: true,
+ line_source: '* [ ] Task 1', line_number: 5)
+
+ expect(toggler.execute).to be_truthy
+ expect(toggler.updated_markdown.lines[4]).to eq "* [x] Task 1\n"
+ expect(toggler.updated_markdown_html).to include('disabled checked> Task 1')
+ end
end
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 19e1c5ff3b2..8d30f5018dd 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TestHooks::ProjectService do
diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb
index 74d7715e50f..799b57eb04e 100644
--- a/spec/services/test_hooks/system_service_spec.rb
+++ b/spec/services/test_hooks/system_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TestHooks::SystemService do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 8631f3f9a33..9ee23f3eb48 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TodoService do
@@ -272,28 +274,6 @@ describe TodoService do
end
end
- describe '#reassigned_issue' do
- it 'creates a pending todo for new assignee' do
- unassigned_issue.assignees << john_doe
- service.reassigned_issue(unassigned_issue, author)
-
- should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo if unassigned' do
- issue.assignees.destroy_all # rubocop: disable DestroyAll
-
- should_not_create_any_todo { service.reassigned_issue(issue, author) }
- end
-
- it 'creates a todo if new assignee is the current user' do
- unassigned_issue.assignees << john_doe
- service.reassigned_issue(unassigned_issue, john_doe)
-
- should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED)
- end
- end
-
describe '#mark_pending_todos_as_done' do
it 'marks related pending todos to the target for the user as done' do
first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author)
@@ -504,10 +484,60 @@ describe TodoService do
end
end
+ describe '#reassigned_issuable' do
+ shared_examples 'reassigned issuable' do
+ it 'creates a pending todo for new assignee' do
+ issuable_unassigned.assignees = [john_doe]
+ service.reassigned_issuable(issuable_unassigned, author)
+
+ should_create_todo(user: john_doe, target: issuable_unassigned, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo if unassigned' do
+ issuable_assigned.assignees = []
+
+ should_not_create_any_todo { service.reassigned_issuable(issuable_assigned, author) }
+ end
+
+ it 'creates a todo if new assignee is the current user' do
+ issuable_assigned.assignees = [john_doe]
+ service.reassigned_issuable(issuable_assigned, john_doe)
+
+ should_create_todo(user: john_doe, target: issuable_assigned, author: john_doe, action: Todo::ASSIGNED)
+ end
+
+ it 'does not create a todo for guests' do
+ service.reassigned_issuable(issuable_assigned, author)
+ should_not_create_todo(user: guest, target: issuable_assigned, action: Todo::MENTIONED)
+ end
+
+ it 'does not create a directly addressed todo for guests' do
+ service.reassigned_issuable(addressed_issuable_assigned, author)
+ should_not_create_todo(user: guest, target: addressed_issuable_assigned, action: Todo::DIRECTLY_ADDRESSED)
+ end
+ end
+
+ context 'issuable is a merge request' do
+ it_behaves_like 'reassigned issuable' do
+ let(:issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issuable_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:issuable_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) }
+ end
+ end
+
+ context 'issuable is an issue' do
+ it_behaves_like 'reassigned issuable' do
+ let(:issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_issuable_assigned) { create(:issue, project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:issuable_unassigned) { create(:issue, project: project, author: author, assignees: []) }
+ end
+ end
+ end
+
describe 'Merge Requests' do
- let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
- let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
- let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) }
+ let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") }
+ let(:addressed_mr_assigned) { create(:merge_request, source_project: project, author: author, assignees: [john_doe], description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") }
+ let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignees: []) }
describe '#new_merge_request' do
it 'creates a pending todo if assigned' do
@@ -659,38 +689,6 @@ describe TodoService do
end
end
- describe '#reassigned_merge_request' do
- it 'creates a pending todo for new assignee' do
- mr_unassigned.update_attribute(:assignee, john_doe)
- service.reassigned_merge_request(mr_unassigned, author)
-
- should_create_todo(user: john_doe, target: mr_unassigned, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo if unassigned' do
- mr_assigned.update_attribute(:assignee, nil)
-
- should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) }
- end
-
- it 'creates a todo if new assignee is the current user' do
- mr_assigned.update_attribute(:assignee, john_doe)
- service.reassigned_merge_request(mr_assigned, john_doe)
-
- should_create_todo(user: john_doe, target: mr_assigned, author: john_doe, action: Todo::ASSIGNED)
- end
-
- it 'does not create a todo for guests' do
- service.reassigned_merge_request(mr_assigned, author)
- should_not_create_todo(user: guest, target: mr_assigned, action: Todo::MENTIONED)
- end
-
- it 'does not create a directly addressed todo for guests' do
- service.reassigned_merge_request(addressed_mr_assigned, author)
- should_not_create_todo(user: guest, target: addressed_mr_assigned, action: Todo::DIRECTLY_ADDRESSED)
- end
- end
-
describe '#merge_merge_request' do
it 'marks related pending todos to the target for the user as done' do
first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author)
diff --git a/spec/services/todos/destroy/confidential_issue_service_spec.rb b/spec/services/todos/destroy/confidential_issue_service_spec.rb
index 3294f7509aa..9f7e656f7d3 100644
--- a/spec/services/todos/destroy/confidential_issue_service_spec.rb
+++ b/spec/services/todos/destroy/confidential_issue_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Todos::Destroy::ConfidentialIssueService do
@@ -7,36 +9,60 @@ describe Todos::Destroy::ConfidentialIssueService do
let(:assignee) { create(:user) }
let(:guest) { create(:user) }
let(:project_member) { create(:user) }
- let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) }
-
- let!(:todo_issue_non_member) { create(:todo, user: user, target: issue, project: project) }
- let!(:todo_issue_member) { create(:todo, user: project_member, target: issue, project: project) }
- let!(:todo_issue_author) { create(:todo, user: author, target: issue, project: project) }
- let!(:todo_issue_asignee) { create(:todo, user: assignee, target: issue, project: project) }
- let!(:todo_issue_guest) { create(:todo, user: guest, target: issue, project: project) }
- let!(:todo_another_non_member) { create(:todo, user: user, project: project) }
+ let(:issue_1) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
describe '#execute' do
before do
project.add_developer(project_member)
project.add_guest(guest)
+
+ # todos not to be deleted
+ create(:todo, user: project_member, target: issue_1, project: project)
+ create(:todo, user: author, target: issue_1, project: project)
+ create(:todo, user: assignee, target: issue_1, project: project)
+ create(:todo, user: user, project: project)
+ # Todos to be deleted
+ create(:todo, user: guest, target: issue_1, project: project)
+ create(:todo, user: user, target: issue_1, project: project)
end
- subject { described_class.new(issue.id).execute }
+ subject { described_class.new(issue_id: issue_1.id).execute }
- context 'when provided issue is confidential' do
- before do
- issue.update!(confidential: true)
+ context 'when issue_id parameter is present' do
+ context 'when provided issue is confidential' do
+ it 'removes issue todos for users who can not access the confidential issue' do
+ expect { subject }.to change { Todo.count }.from(6).to(4)
+ end
end
- it 'removes issue todos for users who can not access the confidential issue' do
- expect { subject }.to change { Todo.count }.from(6).to(4)
+ context 'when provided issue is not confidential' do
+ it 'does not remove any todos' do
+ issue_1.update(confidential: false)
+
+ expect { subject }.not_to change { Todo.count }
+ end
end
end
- context 'when provided issue is not confidential' do
- it 'does not remove any todos' do
- expect { subject }.not_to change { Todo.count }
+ context 'when project_id parameter is present' do
+ subject { described_class.new(issue_id: nil, project_id: project.id).execute }
+
+ it 'removes issues todos for users that cannot access confidential issues' do
+ issue_2 = create(:issue, :confidential, project: project)
+ issue_3 = create(:issue, :confidential, project: project, author: author, assignees: [assignee])
+ issue_4 = create(:issue, project: project)
+ # Todos not to be deleted
+ create(:todo, user: guest, target: issue_1, project: project)
+ create(:todo, user: assignee, target: issue_1, project: project)
+ create(:todo, user: project_member, target: issue_2, project: project)
+ create(:todo, user: author, target: issue_3, project: project)
+ create(:todo, user: user, target: issue_4, project: project)
+ create(:todo, user: user, project: project)
+ # Todos to be deleted
+ create(:todo, user: user, target: issue_1, project: project)
+ create(:todo, user: guest, target: issue_2, project: project)
+
+ expect { subject }.to change { Todo.count }.from(14).to(10)
end
end
end
diff --git a/spec/services/todos/destroy/entity_leave_service_spec.rb b/spec/services/todos/destroy/entity_leave_service_spec.rb
index 4b238280848..2a553e18807 100644
--- a/spec/services/todos/destroy/entity_leave_service_spec.rb
+++ b/spec/services/todos/destroy/entity_leave_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Todos::Destroy::EntityLeaveService do
@@ -73,6 +75,13 @@ describe Todos::Destroy::EntityLeaveService do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
+ it 'enqueues the PrivateFeaturesWorker' do
+ expect(TodosDestroyer::PrivateFeaturesWorker)
+ .to receive(:perform_async).with(project.id, user.id)
+
+ subject
+ end
+
context 'confidential issues' do
context 'when a user is not an author of confidential issue' do
it 'removes only confidential issues todos' do
@@ -244,6 +253,13 @@ describe Todos::Destroy::EntityLeaveService do
project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
+ it 'enqueues the PrivateFeaturesWorker' do
+ expect(TodosDestroyer::PrivateFeaturesWorker)
+ .to receive(:perform_async).with(project.id, user.id)
+
+ subject
+ end
+
context 'when user is not member' do
it 'removes only confidential issues todos' do
expect { subject }.to change { Todo.count }.from(5).to(4)
diff --git a/spec/services/todos/destroy/group_private_service_spec.rb b/spec/services/todos/destroy/group_private_service_spec.rb
index 5cefbdd35ab..a1798686d7c 100644
--- a/spec/services/todos/destroy/group_private_service_spec.rb
+++ b/spec/services/todos/destroy/group_private_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Todos::Destroy::GroupPrivateService do
diff --git a/spec/services/todos/destroy/private_features_service_spec.rb b/spec/services/todos/destroy/private_features_service_spec.rb
index be8b5bb3979..7831e3a47e0 100644
--- a/spec/services/todos/destroy/private_features_service_spec.rb
+++ b/spec/services/todos/destroy/private_features_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Todos::Destroy::PrivateFeaturesService do
diff --git a/spec/services/todos/destroy/project_private_service_spec.rb b/spec/services/todos/destroy/project_private_service_spec.rb
index 128d3487514..7c0c76b6c29 100644
--- a/spec/services/todos/destroy/project_private_service_spec.rb
+++ b/spec/services/todos/destroy/project_private_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Todos::Destroy::ProjectPrivateService do
diff --git a/spec/services/update_deployment_service_spec.rb b/spec/services/update_deployment_service_spec.rb
index 3c55dd9659a..7dc52f6816a 100644
--- a/spec/services/update_deployment_service_spec.rb
+++ b/spec/services/update_deployment_service_spec.rb
@@ -1,7 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UpdateDeploymentService do
let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
let(:options) { { name: 'production' } }
let(:job) do
@@ -13,24 +16,23 @@ describe UpdateDeploymentService do
project: project)
end
- let(:project) { create(:project, :repository) }
- let(:environment) { deployment.environment }
let(:deployment) { job.deployment }
- let(:service) { described_class.new(deployment) }
+ let(:environment) { deployment.environment }
+
+ subject(:service) { described_class.new(deployment) }
before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
job.success! # Create/Succeed deployment
end
describe '#execute' do
- subject { service.execute }
-
let(:store) { Gitlab::EtagCaching::Store.new }
it 'invalidates the environment etag cache' do
old_value = store.get(environment.etag_cache_key)
- subject
+ service.execute
expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
end
@@ -40,14 +42,30 @@ describe UpdateDeploymentService do
.to receive(:create_ref)
.with(deployment.ref, deployment.send(:ref_path))
- subject
+ service.execute
end
it 'updates merge request metrics' do
expect_any_instance_of(Deployment)
.to receive(:update_merge_request_metrics!)
- subject
+ service.execute
+ end
+
+ it 'returns the deployment' do
+ expect(subject.execute).to eq(deployment)
+ end
+
+ it 'returns the deployment when could not save the environment' do
+ allow(environment).to receive(:save).and_return(false)
+
+ expect(subject.execute).to eq(deployment)
+ end
+
+ it 'returns the deployment when environment is stopped' do
+ allow(environment).to receive(:stopped?).and_return(true)
+
+ expect(subject.execute).to eq(deployment)
end
context 'when start action is defined' do
@@ -59,7 +77,7 @@ describe UpdateDeploymentService do
end
it 'makes environment available' do
- subject
+ service.execute
expect(environment.reload).to be_available
end
@@ -78,11 +96,11 @@ describe UpdateDeploymentService do
end
it 'does not create a new environment' do
- expect { subject }.not_to change { Environment.count }
+ expect { subject.execute }.not_to change { Environment.count }
end
it 'updates external url' do
- subject
+ subject.execute
expect(subject.environment.name).to eq('review-apps/master')
expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com')
diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb
index 812dd42934d..12a2b287c72 100644
--- a/spec/services/update_merge_request_metrics_service_spec.rb
+++ b/spec/services/update_merge_request_metrics_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe MergeRequestMetricsService do
diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb
index ef535c5cf1f..23ea4e003f8 100644
--- a/spec/services/update_snippet_service_spec.rb
+++ b/spec/services/update_snippet_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UpdateSnippetService do
diff --git a/spec/services/upload_service_spec.rb b/spec/services/upload_service_spec.rb
index 4a809d5bf18..504e61f9903 100644
--- a/spec/services/upload_service_spec.rb
+++ b/spec/services/upload_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UploadService do
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index 87a90378e2b..902ed723e09 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UserProjectAccessChangedService do
diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb
index 3c0a4ac8e18..d8d2be87fd3 100644
--- a/spec/services/users/activity_service_spec.rb
+++ b/spec/services/users/activity_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::ActivityService do
@@ -76,7 +78,7 @@ describe Users::ActivityService do
let(:last_activity_on) { nil }
it 'does not update last_activity_on' do
- stub_exclusive_lease_taken("acitvity_service:#{user.id}", timeout: 1.minute.to_i)
+ stub_exclusive_lease_taken("activity_service:#{user.id}", timeout: 1.minute.to_i)
expect { subject.execute }.not_to change(user, :last_activity_on)
end
diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb
index b7b9817efdb..aed5d2598ef 100644
--- a/spec/services/users/build_service_spec.rb
+++ b/spec/services/users/build_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::BuildService do
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
index 24dac569678..a139dc01314 100644
--- a/spec/services/users/create_service_spec.rb
+++ b/spec/services/users/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::CreateService do
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 83f1495a1c6..4a5f4509a7b 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::DestroyService do
@@ -78,7 +80,7 @@ describe Users::DestroyService do
end
context "for an merge request the user was assigned to" do
- let!(:merge_request) { create(:merge_request, source_project: project, assignee: user) }
+ let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
before do
service.execute(user)
@@ -91,7 +93,7 @@ describe Users::DestroyService do
it 'migrates the merge request so that it is "Unassigned"' do
migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
- expect(migrated_merge_request.assignee).to be_nil
+ expect(migrated_merge_request.assignees).to be_empty
end
end
end
@@ -208,6 +210,8 @@ describe Users::DestroyService do
describe "calls the before/after callbacks" do
it 'of project_members' do
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:initialize).once
expect_any_instance_of(ProjectMember).to receive(:run_callbacks).with(:destroy).once
service.execute(user)
@@ -217,6 +221,8 @@ describe Users::DestroyService do
group_member = create(:group_member)
group_member.group.group_members.create(user: user, access_level: 40)
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
+ expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:destroy).once
service.execute(user)
diff --git a/spec/services/users/last_push_event_service_spec.rb b/spec/services/users/last_push_event_service_spec.rb
index 2b6c0267a0f..424e9e2f8ef 100644
--- a/spec/services/users/last_push_event_service_spec.rb
+++ b/spec/services/users/last_push_event_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::LastPushEventService do
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
index 68b0f79c6d1..40206775aed 100644
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::MigrateToGhostUserService do
@@ -78,7 +80,7 @@ describe Users::MigrateToGhostUserService do
context "when record migration fails with a rollback exception" do
before do
- expect_any_instance_of(MergeRequest::ActiveRecord_Associations_CollectionProxy)
+ expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ActiveRecord::Rollback)
end
diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb
index 122b96ef216..0287a24808d 100644
--- a/spec/services/users/refresh_authorized_projects_service_spec.rb
+++ b/spec/services/users/refresh_authorized_projects_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::RefreshAuthorizedProjectsService do
diff --git a/spec/services/users/respond_to_terms_service_spec.rb b/spec/services/users/respond_to_terms_service_spec.rb
index fb08dd10b87..d840706e8a5 100644
--- a/spec/services/users/respond_to_terms_service_spec.rb
+++ b/spec/services/users/respond_to_terms_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::RespondToTermsService do
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index 529c8485202..9384287f98a 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Users::UpdateService do
@@ -11,6 +13,15 @@ describe Users::UpdateService do
expect(user.name).to eq('New Name')
end
+ it 'updates time preferences' do
+ result = update_user(user, timezone: 'Europe/Warsaw', time_display_relative: true, time_format_in_24h: false)
+
+ expect(result).to eq(status: :success)
+ expect(user.reload.timezone).to eq('Europe/Warsaw')
+ expect(user.time_display_relative).to eq(true)
+ expect(user.time_format_in_24h).to eq(false)
+ end
+
it 'returns an error result when record cannot be updated' do
result = {}
expect do
diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb
index d974cc0226f..f2b3b44d223 100644
--- a/spec/services/verify_pages_domain_service_spec.rb
+++ b/spec/services/verify_pages_domain_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe VerifyPagesDomainService do
@@ -9,88 +11,177 @@ describe VerifyPagesDomainService do
subject(:service) { described_class.new(domain) }
describe '#execute' do
- context 'verification code recognition (verified domain)' do
- where(:domain_sym, :code_sym) do
- :domain | :verification_code
- :domain | :keyed_verification_code
-
- :verification_domain | :verification_code
- :verification_domain | :keyed_verification_code
- end
+ where(:domain_sym, :code_sym) do
+ :domain | :verification_code
+ :domain | :keyed_verification_code
- with_them do
- set(:domain) { create(:pages_domain) }
+ :verification_domain | :verification_code
+ :verification_domain | :keyed_verification_code
+ end
- let(:domain_name) { domain.send(domain_sym) }
- let(:verification_code) { domain.send(code_sym) }
+ with_them do
+ let(:domain_name) { domain.send(domain_sym) }
+ let(:verification_code) { domain.send(code_sym) }
+ shared_examples 'verifies and enables the domain' do
it 'verifies and enables the domain' do
- stub_resolver(domain_name => ['something else', verification_code])
-
expect(service.execute).to eq(status: :success)
+
expect(domain).to be_verified
expect(domain).to be_enabled
+ expect(domain.remove_at).to be_nil
end
+ end
- it 'verifies and enables when the code is contained partway through a TXT record' do
- stub_resolver(domain_name => "something #{verification_code} else")
+ shared_examples 'successful enablement and verification' do
+ context 'when txt record contains verification code' do
+ before do
+ stub_resolver(domain_name => ['something else', verification_code])
+ end
- expect(service.execute).to eq(status: :success)
- expect(domain).to be_verified
- expect(domain).to be_enabled
+ include_examples 'verifies and enables the domain'
end
- it 'does not verify when the code is not present' do
- stub_resolver(domain_name => 'something else')
+ context 'when txt record contains verification code with other text' do
+ before do
+ stub_resolver(domain_name => "something #{verification_code} else")
+ end
- expect(service.execute).to eq(error_status)
+ include_examples 'verifies and enables the domain'
+ end
+ end
+ shared_examples 'unverifies and disables domain' do
+ it 'unverifies domain' do
+ expect(service.execute).to eq(error_status)
expect(domain).not_to be_verified
- expect(domain).to be_enabled
+ end
+
+ it 'disables domain and shedules it for removal in 1 week' do
+ service.execute
+
+ expect(domain).not_to be_enabled
+
+ expect(domain.remove_at).to be_like_time(7.days.from_now)
end
end
- context 'verified domain' do
- set(:domain) { create(:pages_domain) }
+ context 'when domain is disabled(or new)' do
+ let(:domain) { create(:pages_domain, :disabled) }
- it 'unverifies (but does not disable) when the right code is not present' do
- stub_resolver(domain.domain => 'something else')
+ include_examples 'successful enablement and verification'
- expect(service.execute).to eq(error_status)
- expect(domain).not_to be_verified
- expect(domain).to be_enabled
+ context 'when txt record does not contain verification code' do
+ before do
+ stub_resolver(domain_name => 'something else')
+ end
+
+ include_examples 'unverifies and disables domain'
end
- it 'unverifies (but does not disable) when no records are present' do
- stub_resolver
+ context 'when txt record does not contain verification code' do
+ before do
+ stub_resolver(domain_name => 'something else')
+ end
- expect(service.execute).to eq(error_status)
- expect(domain).not_to be_verified
- expect(domain).to be_enabled
+ include_examples 'unverifies and disables domain'
+ end
+
+ context 'when no txt records are present' do
+ before do
+ stub_resolver
+ end
+
+ include_examples 'unverifies and disables domain'
end
end
- context 'expired domain' do
- set(:domain) { create(:pages_domain, :expired) }
+ context 'when domain is verified' do
+ let(:domain) { create(:pages_domain) }
- it 'verifies and enables when the right code is present' do
- stub_resolver(domain.domain => domain.keyed_verification_code)
+ include_examples 'successful enablement and verification'
- expect(service.execute).to eq(status: :success)
+ shared_examples 'unverifing domain' do
+ it 'unverifies but does not disable domain' do
+ expect(service.execute).to eq(error_status)
+ expect(domain).not_to be_verified
+ expect(domain).to be_enabled
+ end
- expect(domain).to be_verified
- expect(domain).to be_enabled
+ it 'does not schedule domain for removal' do
+ service.execute
+ expect(domain.remove_at).to be_nil
+ end
end
- it 'disables when the right code is not present' do
- error_status[:message] += '. It is now disabled.'
+ context 'when txt record does not contain verification code' do
+ before do
+ stub_resolver(domain_name => 'something else')
+ end
- stub_resolver
+ include_examples 'unverifing domain'
+ end
- expect(service.execute).to eq(error_status)
+ context 'when no txt records are present' do
+ before do
+ stub_resolver
+ end
- expect(domain).not_to be_verified
- expect(domain).not_to be_enabled
+ include_examples 'unverifing domain'
+ end
+ end
+
+ context 'when domain is expired' do
+ let(:domain) { create(:pages_domain, :expired) }
+
+ context 'when the right code is present' do
+ before do
+ stub_resolver(domain_name => domain.keyed_verification_code)
+ end
+
+ include_examples 'verifies and enables the domain'
+ end
+
+ context 'when the right code is not present' do
+ before do
+ stub_resolver
+ end
+
+ let(:error_status) { { status: :error, message: "Couldn't verify #{domain.domain}. It is now disabled." } }
+
+ include_examples 'unverifies and disables domain'
+ end
+ end
+
+ context 'when domain is disabled and scheduled for removal' do
+ let(:domain) { create(:pages_domain, :disabled, :scheduled_for_removal) }
+
+ context 'when the right code is present' do
+ before do
+ stub_resolver(domain.domain => domain.keyed_verification_code)
+ end
+
+ it 'verifies and enables domain' do
+ expect(service.execute).to eq(status: :success)
+
+ expect(domain).to be_verified
+ expect(domain).to be_enabled
+ end
+
+ it 'prevent domain from being removed' do
+ expect { service.execute }.to change { domain.remove_at }.to(nil)
+ end
+ end
+
+ context 'when the right code is not present' do
+ before do
+ stub_resolver
+ end
+
+ it 'keeps domain scheduled for removal but does not change removal time' do
+ expect { service.execute }.not_to change { domain.remove_at }
+ expect(domain.remove_at).to be_present
+ end
end
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 747e04fb18c..37bafc0c002 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe WebHookService do
+ include StubRequests
+
let(:project) { create(:project) }
let(:project_hook) { create(:project_hook) }
let(:headers) do
@@ -65,11 +69,11 @@ describe WebHookService do
let(:project_hook) { create(:project_hook, url: 'https://demo:demo@example.org/') }
it 'uses the credentials' do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
service_instance.execute
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
headers: headers.merge('Authorization' => 'Basic ZGVtbzpkZW1v')
).once
end
@@ -80,11 +84,11 @@ describe WebHookService do
let(:project_hook) { create(:project_hook, url: 'https://demo@example.org/') }
it 'uses the credentials anyways' do
- WebMock.stub_request(:post, url)
+ stub_full_request(url, method: :post)
service_instance.execute
- expect(WebMock).to have_requested(:post, url).with(
+ expect(WebMock).to have_requested(:post, stubbed_hostname(url)).with(
headers: headers.merge('Authorization' => 'Basic ZGVtbzo=')
).once
end
diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb
index 259f445247e..84510dcf700 100644
--- a/spec/services/wiki_pages/create_service_spec.rb
+++ b/spec/services/wiki_pages/create_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe WikiPages::CreateService do
diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb
index 2938126914b..c74eac4dad6 100644
--- a/spec/services/wiki_pages/destroy_service_spec.rb
+++ b/spec/services/wiki_pages/destroy_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe WikiPages::DestroyService do
diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb
index 2399db7d3d4..19866bd3bfc 100644
--- a/spec/services/wiki_pages/update_service_spec.rb
+++ b/spec/services/wiki_pages/update_service_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe WikiPages::UpdateService do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 97e7a019222..390a869d93f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -44,6 +44,8 @@ Dir[Rails.root.join("spec/support/shared_contexts/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/support/shared_examples/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
+quality_level = Quality::TestLevel.new
+
RSpec.configure do |config|
config.use_transactional_fixtures = false
config.use_instantiated_fixtures = false
@@ -53,10 +55,12 @@ RSpec.configure do |config|
config.display_try_failure_messages = true
config.infer_spec_type_from_file_location!
+ config.full_backtrace = !!ENV['CI']
- config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
+ config.define_derived_metadata(file_path: %r{(ee)?/spec/.+_spec\.rb\z}) do |metadata|
location = metadata[:location]
+ metadata[:level] = quality_level.level_for(location)
metadata[:api] = true if location =~ %r{/spec/requests/api/}
# do not overwrite type if it's already set
@@ -66,6 +70,7 @@ RSpec.configure do |config|
metadata[:type] = match[1].singularize.to_sym if match
end
+ config.include LicenseHelpers
config.include ActiveJob::TestHelper
config.include ActiveSupport::Testing::TimeHelpers
config.include CycleAnalyticsHelpers
@@ -82,6 +87,7 @@ RSpec.configure do |config|
config.include Devise::Test::IntegrationHelpers, type: :feature
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
+ config.include WaitHelpers, type: :feature
config.include EmailHelpers, :mailer, type: :mailer
config.include Warden::Test::Helpers, type: :request
config.include Gitlab::Routing, type: :routing
@@ -96,6 +102,7 @@ RSpec.configure do |config|
config.include MigrationsHelpers, :migration
config.include RedisHelpers
config.include Rails.application.routes.url_helpers, type: :routing
+ config.include PolicyHelpers, type: :policy
if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing.
@@ -115,10 +122,17 @@ RSpec.configure do |config|
TestEnv.clean_test_path
end
- config.before do
+ config.before do |example|
# Enable all features by default for testing
allow(Feature).to receive(:enabled?) { true }
+ enabled = example.metadata[:enable_rugged].present?
+
+ # Disable Rugged features by default
+ Gitlab::Git::RuggedImpl::Repository::FEATURE_FLAGS.each do |flag|
+ allow(Feature).to receive(:enabled?).with(flag).and_return(enabled)
+ end
+
# The following can be removed when we remove the staged rollout strategy
# and we can just enable it using instance wide settings
# (ie. ApplicationSetting#auto_devops_enabled)
@@ -127,7 +141,7 @@ RSpec.configure do |config|
.and_return(false)
end
- config.before(:example, :quarantine) do
+ config.around(:example, :quarantine) do
# Skip tests in quarantine unless we explicitly focus on them.
skip('In quarantine') unless config.inclusion_filter[:quarantine]
end
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index 5f709831ce1..63b719be03e 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -72,6 +72,15 @@ shared_examples_for 'group and project milestones' do |route_definition|
expect(json_response.first['id']).to eq closed_milestone.id
end
+ it 'returns a milestone by title' do
+ get api(route, user), params: { title: 'version2' }
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['title']).to eq milestone.title
+ expect(json_response.first['id']).to eq milestone.id
+ end
+
it 'returns a milestone by searching for title' do
get api(route, user), params: { search: 'version2' }
diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb
index 6591d56e473..4cf34d43117 100644
--- a/spec/support/api/schema_matcher.rb
+++ b/spec/support/api/schema_matcher.rb
@@ -1,10 +1,16 @@
module SchemaPath
- def self.expand(schema, dir = '')
- Rails.root.join('spec', dir, "fixtures/api/schemas/#{schema}.json").to_s
+ def self.expand(schema, dir = nil)
+ if Gitlab.ee? && dir.nil?
+ ee_path = expand(schema, 'ee')
+
+ return ee_path if File.exist?(ee_path)
+ end
+
+ Rails.root.join(dir.to_s, 'spec', "fixtures/api/schemas/#{schema}.json").to_s
end
end
-RSpec::Matchers.define :match_response_schema do |schema, dir: '', **options|
+RSpec::Matchers.define :match_response_schema do |schema, dir: nil, **options|
match do |response|
@errors = JSON::Validator.fully_validate(
SchemaPath.expand(schema, dir), response.body, options)
@@ -18,8 +24,16 @@ RSpec::Matchers.define :match_response_schema do |schema, dir: '', **options|
end
end
-RSpec::Matchers.define :match_schema do |schema, dir: '', **options|
+RSpec::Matchers.define :match_schema do |schema, dir: nil, **options|
match do |data|
- JSON::Validator.validate!(SchemaPath.expand(schema, dir), data, options)
+ @errors = JSON::Validator.fully_validate(
+ SchemaPath.expand(schema, dir), data, options)
+
+ @errors.empty?
+ end
+
+ failure_message do |response|
+ "didn't match the schema defined by #{SchemaPath.expand(schema, dir)}" \
+ " The validation errors were:\n#{@errors.join("\n")}"
end
end
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index e883d33f671..15037222630 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -3,6 +3,8 @@ shared_examples 'an unauthorized API user' do
end
shared_examples 'time tracking endpoints' do |issuable_name|
+ let(:non_member) { create(:user) }
+
issuable_collection_name = issuable_name.pluralize
describe "POST /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_estimate" do
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 18a7a392c12..875a9a76e12 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -17,6 +17,8 @@ JS_CONSOLE_FILTER = Regexp.union([
"Download the Vue Devtools extension"
])
+CAPYBARA_WINDOW_SIZE = [1366, 768].freeze
+
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
# This enables access to logs with `page.driver.manage.get_log(:browser)`
@@ -29,7 +31,7 @@ Capybara.register_driver :chrome do |app|
)
options = Selenium::WebDriver::Chrome::Options.new
- options.add_argument("window-size=1240,1400")
+ options.add_argument("window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}")
# Chrome won't work properly in a Docker container in sandbox mode
options.add_argument("no-sandbox")
@@ -78,8 +80,11 @@ RSpec.configure do |config|
protocol: 'http')
# reset window size between tests
- unless session.current_window.size == [1240, 1400]
- session.current_window.resize_to(1240, 1400) rescue nil
+ unless session.current_window.size == CAPYBARA_WINDOW_SIZE
+ begin
+ session.current_window.resize_to(*CAPYBARA_WINDOW_SIZE)
+ rescue # ?
+ end
end
end
diff --git a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
index 72912ffb89d..a0c77eecb61 100644
--- a/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
+++ b/spec/support/controllers/ldap_omniauth_callbacks_controller_shared_context.rb
@@ -25,9 +25,13 @@ shared_context 'Ldap::OmniauthCallbacksController' do
described_class.define_providers!
Rails.application.reload_routes!
- mock_auth_hash(provider.to_s, uid, user.email)
+ @original_env_config_omniauth_auth = mock_auth_hash(provider.to_s, uid, user.email)
stub_omniauth_provider(provider, context: request)
allow(Gitlab::Auth::LDAP::Access).to receive(:allowed?).and_return(valid_login?)
end
+
+ after do
+ Rails.application.env_config['omniauth.auth'] = @original_env_config_omniauth_auth
+ end
end
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb
new file mode 100644
index 00000000000..edd7de94203
--- /dev/null
+++ b/spec/support/database_cleaner.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'database_cleaner/active_record/deletion'
+require_relative 'db_cleaner'
+
+module FakeInformationSchema
+ # Work around a bug in DatabaseCleaner when using the deletion strategy:
+ # https://github.com/DatabaseCleaner/database_cleaner/issues/347
+ #
+ # On MySQL, if the information schema is said to exist, we use an inaccurate
+ # row count leading to some tables not being cleaned when they should
+ def information_schema_exists?(_connection)
+ false
+ end
+end
+
+DatabaseCleaner::ActiveRecord::Deletion.prepend(FakeInformationSchema)
+
+RSpec.configure do |config|
+ include DbCleaner
+
+ # Ensure all sequences are reset at the start of the suite run
+ config.before(:suite) do
+ setup_database_cleaner
+ DatabaseCleaner.clean_with(:truncation)
+ end
+
+ config.append_after(:context) do
+ DatabaseCleaner.clean_with(:deletion, cache_tables: false)
+ end
+
+ config.before do
+ setup_database_cleaner
+ DatabaseCleaner.strategy = :transaction
+ end
+
+ config.before(:each, :js) do
+ DatabaseCleaner.strategy = :deletion, { except: deletion_except_tables, cache_tables: false }
+ end
+
+ config.before(:each, :delete) do
+ DatabaseCleaner.strategy = :deletion, { except: deletion_except_tables, cache_tables: false }
+ end
+
+ config.before(:each, :migration) do
+ DatabaseCleaner.strategy = :deletion, { cache_tables: false }
+ end
+
+ config.before do
+ DatabaseCleaner.start
+ end
+
+ config.append_after do
+ DatabaseCleaner.clean
+ end
+end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 34b9efaaecd..c69fa322073 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -1,49 +1,9 @@
-require 'database_cleaner/active_record/deletion'
-
-module FakeInformationSchema
- # Work around a bug in DatabaseCleaner when using the deletion strategy:
- # https://github.com/DatabaseCleaner/database_cleaner/issues/347
- #
- # On MySQL, if the information schema is said to exist, we use an inaccurate
- # row count leading to some tables not being cleaned when they should
- def information_schema_exists?(_connection)
- false
- end
-end
-
-DatabaseCleaner::ActiveRecord::Deletion.prepend(FakeInformationSchema)
-
-RSpec.configure do |config|
- # Ensure all sequences are reset at the start of the suite run
- config.before(:suite) do
- DatabaseCleaner.clean_with(:truncation)
- end
-
- config.append_after(:context) do
- DatabaseCleaner.clean_with(:deletion, cache_tables: false)
- end
-
- config.before do
- DatabaseCleaner.strategy = :transaction
- end
-
- config.before(:each, :js) do
- DatabaseCleaner.strategy = :deletion, { cache_tables: false }
- end
-
- config.before(:each, :delete) do
- DatabaseCleaner.strategy = :deletion, { cache_tables: false }
- end
-
- config.before(:each, :migration) do
- DatabaseCleaner.strategy = :deletion, { cache_tables: false }
- end
-
- config.before do
- DatabaseCleaner.start
+module DbCleaner
+ def deletion_except_tables
+ []
end
- config.append_after do
- DatabaseCleaner.clean
+ def setup_database_cleaner
+ DatabaseCleaner[:active_record, { connection: ActiveRecord::Base }]
end
end
diff --git a/spec/support/external_authorization_service_helpers.rb b/spec/support/external_authorization_service_helpers.rb
new file mode 100644
index 00000000000..79dd9a3d58e
--- /dev/null
+++ b/spec/support/external_authorization_service_helpers.rb
@@ -0,0 +1,33 @@
+module ExternalAuthorizationServiceHelpers
+ def enable_external_authorization_service_check
+ stub_application_setting(external_authorization_service_enabled: true)
+
+ stub_application_setting(external_authorization_service_url: 'https://authorize.me')
+ stub_application_setting(external_authorization_service_default_label: 'default_label')
+ stub_request(:post, "https://authorize.me").to_return(status: 200)
+ end
+
+ def external_service_set_access(allowed, user, project)
+ enable_external_authorization_service_check
+ classification_label = ::Gitlab::CurrentSettings.current_application_settings
+ .external_authorization_service_default_label
+
+ # Reload the project so cached licensed features are reloaded
+ if project
+ classification_label = Project.find(project.id).external_authorization_classification_label
+ end
+
+ allow(::Gitlab::ExternalAuthorization)
+ .to receive(:access_allowed?)
+ .with(user, classification_label, any_args)
+ .and_return(allowed)
+ end
+
+ def external_service_allow_access(user, project = nil)
+ external_service_set_access(true, user, project)
+ end
+
+ def external_service_deny_access(user, project = nil)
+ external_service_set_access(false, user, project)
+ end
+end
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index 42a086d58d2..542f533d590 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -7,7 +7,7 @@ shared_examples 'discussion comments' do |resource_name|
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
let(:comments_selector) { '.timeline > .note.timeline-entry' }
- it 'clicking "Comment" will post a comment' do
+ it 'clicking "Comment" will post a comment', :quarantine do
expect(page).to have_selector toggle_selector
find("#{form_selector} .note-textarea").send_keys('a')
@@ -224,7 +224,7 @@ shared_examples 'discussion comments' do |resource_name|
find(toggle_selector).click
end
- it 'should have "Start discussion" selected' do
+ it 'has "Start discussion" selected' do
find("#{menu_selector} li", match: :first)
items = all("#{menu_selector} li")
@@ -267,7 +267,7 @@ shared_examples 'discussion comments' do |resource_name|
end
end
- it 'should have "Comment" selected when opening the menu' do
+ it 'has "Comment" selected when opening the menu' do
find(toggle_selector).click
find("#{menu_selector} li", match: :first)
diff --git a/spec/support/features/issuable_quick_actions_shared_examples.rb b/spec/support/features/issuable_quick_actions_shared_examples.rb
deleted file mode 100644
index 2a883ce1074..00000000000
--- a/spec/support/features/issuable_quick_actions_shared_examples.rb
+++ /dev/null
@@ -1,389 +0,0 @@
-# Specifications for behavior common to all objects with executable attributes.
-# It takes a `issuable_type`, and expect an `issuable`.
-
-shared_examples 'issuable record that supports quick actions in its description and notes' do |issuable_type|
- include Spec::Support::Helpers::Features::NotesHelpers
-
- let(:maintainer) { create(:user) }
- let(:project) do
- case issuable_type
- when :merge_request
- create(:project, :public, :repository)
- when :issue
- create(:project, :public)
- end
- end
- let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
- let!(:label_bug) { create(:label, project: project, title: 'bug') }
- let!(:label_feature) { create(:label, project: project, title: 'feature') }
- let(:new_url_opts) { {} }
-
- before do
- project.add_maintainer(maintainer)
-
- gitlab_sign_in(maintainer)
- end
-
- after do
- # Ensure all outstanding Ajax requests are complete to avoid database deadlocks
- wait_for_requests
- end
-
- describe "new #{issuable_type}", :js do
- context 'with commands in the description' do
- it "creates the #{issuable_type} and interpret commands accordingly" do
- case issuable_type
- when :merge_request
- visit public_send("namespace_project_new_merge_request_path", project.namespace, project, new_url_opts)
- when :issue
- visit public_send("new_namespace_project_issue_path", project.namespace, project, new_url_opts)
- end
- fill_in "#{issuable_type}_title", with: 'bug 345'
- fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug\n/milestone %\"ASAP\""
- click_button "Submit #{issuable_type}".humanize
-
- issuable = project.public_send(issuable_type.to_s.pluralize).first
-
- expect(issuable.description).to eq "bug description"
- expect(issuable.labels).to eq [label_bug]
- expect(issuable.milestone).to eq milestone
- expect(page).to have_content 'bug 345'
- expect(page).to have_content 'bug description'
- end
- end
- end
-
- describe "note on #{issuable_type}", :js do
- before do
- visit public_send("project_#{issuable_type}_path", project, issuable)
- end
-
- context 'with a note containing commands' do
- it 'creates a note without the commands and interpret the commands accordingly' do
- assignee = create(:user, username: 'bob')
- add_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
-
- expect(page).to have_content 'Awesome!'
- expect(page).not_to have_content '/assign @bob'
- expect(page).not_to have_content '/label ~bug'
- expect(page).not_to have_content '/milestone %"ASAP"'
-
- wait_for_requests
- issuable.reload
- note = issuable.notes.user.first
-
- expect(note.note).to eq "Awesome!"
- expect(issuable.assignees).to eq [assignee]
- expect(issuable.labels).to eq [label_bug]
- expect(issuable.milestone).to eq milestone
- end
-
- it 'removes the quick action from note and explains it in the preview' do
- preview_note("Awesome!\n\n/close")
-
- expect(page).to have_content 'Awesome!'
- expect(page).not_to have_content '/close'
- issuable_name = issuable.is_a?(Issue) ? 'issue' : 'merge request'
- expect(page).to have_content "Closes this #{issuable_name}."
- end
- end
-
- context 'with a note containing only commands' do
- it 'does not create a note but interpret the commands accordingly' do
- assignee = create(:user, username: 'bob')
- add_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
-
- expect(page).not_to have_content '/assign @bob'
- expect(page).not_to have_content '/label ~bug'
- expect(page).not_to have_content '/milestone %"ASAP"'
- expect(page).to have_content 'Commands applied'
-
- issuable.reload
-
- expect(issuable.notes.user).to be_empty
- expect(issuable.assignees).to eq [assignee]
- expect(issuable.labels).to eq [label_bug]
- expect(issuable.milestone).to eq milestone
- end
- end
-
- context "with a note closing the #{issuable_type}" do
- before do
- expect(issuable).to be_open
- end
-
- context "when current user can close #{issuable_type}" do
- it "closes the #{issuable_type}" do
- add_note("/close")
-
- expect(page).not_to have_content '/close'
- expect(page).to have_content 'Commands applied'
-
- expect(issuable.reload).to be_closed
- end
- end
-
- context "when current user cannot close #{issuable_type}" do
- before do
- guest = create(:user)
- project.add_guest(guest)
-
- gitlab_sign_out
- gitlab_sign_in(guest)
- visit public_send("project_#{issuable_type}_path", project, issuable)
- end
-
- it "does not close the #{issuable_type}" do
- add_note("/close")
-
- expect(page).not_to have_content 'Commands applied'
-
- expect(issuable).to be_open
- end
- end
- end
-
- context "with a note reopening the #{issuable_type}" do
- before do
- issuable.close
- expect(issuable).to be_closed
- end
-
- context "when current user can reopen #{issuable_type}" do
- it "reopens the #{issuable_type}" do
- add_note("/reopen")
-
- expect(page).not_to have_content '/reopen'
- expect(page).to have_content 'Commands applied'
-
- expect(issuable.reload).to be_open
- end
- end
-
- context "when current user cannot reopen #{issuable_type}" do
- before do
- guest = create(:user)
- project.add_guest(guest)
-
- gitlab_sign_out
- gitlab_sign_in(guest)
- visit public_send("project_#{issuable_type}_path", project, issuable)
- end
-
- it "does not reopen the #{issuable_type}" do
- add_note("/reopen")
-
- expect(page).not_to have_content 'Commands applied'
-
- expect(issuable).to be_closed
- end
- end
- end
-
- context "with a note changing the #{issuable_type}'s title" do
- context "when current user can change title of #{issuable_type}" do
- it "reopens the #{issuable_type}" do
- add_note("/title Awesome new title")
-
- expect(page).not_to have_content '/title'
- expect(page).to have_content 'Commands applied'
-
- expect(issuable.reload.title).to eq 'Awesome new title'
- end
- end
-
- context "when current user cannot change title of #{issuable_type}" do
- before do
- guest = create(:user)
- project.add_guest(guest)
-
- gitlab_sign_out
- gitlab_sign_in(guest)
- visit public_send("project_#{issuable_type}_path", project, issuable)
- end
-
- it "does not change the #{issuable_type} title" do
- add_note("/title Awesome new title")
-
- expect(page).not_to have_content 'Commands applied'
-
- expect(issuable.reload.title).not_to eq 'Awesome new title'
- end
- end
- end
-
- context "with a note marking the #{issuable_type} as todo" do
- it "creates a new todo for the #{issuable_type}" do
- add_note("/todo")
-
- expect(page).not_to have_content '/todo'
- expect(page).to have_content 'Commands applied'
-
- todos = TodosFinder.new(maintainer).execute
- todo = todos.first
-
- expect(todos.size).to eq 1
- expect(todo).to be_pending
- expect(todo.target).to eq issuable
- expect(todo.author).to eq maintainer
- expect(todo.user).to eq maintainer
- end
- end
-
- context "with a note marking the #{issuable_type} as done" do
- before do
- TodoService.new.mark_todo(issuable, maintainer)
- end
-
- it "creates a new todo for the #{issuable_type}" do
- todos = TodosFinder.new(maintainer).execute
- todo = todos.first
-
- expect(todos.size).to eq 1
- expect(todos.first).to be_pending
- expect(todo.target).to eq issuable
- expect(todo.author).to eq maintainer
- expect(todo.user).to eq maintainer
-
- add_note("/done")
-
- expect(page).not_to have_content '/done'
- expect(page).to have_content 'Commands applied'
-
- expect(todo.reload).to be_done
- end
- end
-
- context "with a note subscribing to the #{issuable_type}" do
- it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(maintainer, project)).to be_falsy
-
- add_note("/subscribe")
-
- expect(page).not_to have_content '/subscribe'
- expect(page).to have_content 'Commands applied'
-
- expect(issuable.subscribed?(maintainer, project)).to be_truthy
- end
- end
-
- context "with a note unsubscribing to the #{issuable_type} as done" do
- before do
- issuable.subscribe(maintainer, project)
- end
-
- it "creates a new todo for the #{issuable_type}" do
- expect(issuable.subscribed?(maintainer, project)).to be_truthy
-
- add_note("/unsubscribe")
-
- expect(page).not_to have_content '/unsubscribe'
- expect(page).to have_content 'Commands applied'
-
- expect(issuable.subscribed?(maintainer, project)).to be_falsy
- end
- end
-
- context "with a note assigning the #{issuable_type} to the current user" do
- it "assigns the #{issuable_type} to the current user" do
- add_note("/assign me")
-
- expect(page).not_to have_content '/assign me'
- expect(page).to have_content 'Commands applied'
-
- expect(issuable.reload.assignees).to eq [maintainer]
- end
- end
-
- context "with a note locking the #{issuable_type} discussion" do
- before do
- issuable.update(discussion_locked: false)
- expect(issuable).not_to be_discussion_locked
- end
-
- context "when current user can lock #{issuable_type} discussion" do
- it "locks the #{issuable_type} discussion" do
- add_note("/lock")
-
- expect(page).not_to have_content '/lock'
- expect(page).to have_content 'Commands applied'
-
- expect(issuable.reload).to be_discussion_locked
- end
- end
-
- context "when current user cannot lock #{issuable_type}" do
- before do
- guest = create(:user)
- project.add_guest(guest)
-
- gitlab_sign_out
- sign_in(guest)
- visit public_send("project_#{issuable_type}_path", project, issuable)
- end
-
- it "does not lock the #{issuable_type} discussion" do
- add_note("/lock")
-
- expect(page).not_to have_content 'Commands applied'
-
- expect(issuable).not_to be_discussion_locked
- end
- end
- end
-
- context "with a note unlocking the #{issuable_type} discussion" do
- before do
- issuable.update(discussion_locked: true)
- expect(issuable).to be_discussion_locked
- end
-
- context "when current user can unlock #{issuable_type} discussion" do
- it "unlocks the #{issuable_type} discussion" do
- add_note("/unlock")
-
- expect(page).not_to have_content '/unlock'
- expect(page).to have_content 'Commands applied'
-
- expect(issuable.reload).not_to be_discussion_locked
- end
- end
-
- context "when current user cannot unlock #{issuable_type}" do
- before do
- guest = create(:user)
- project.add_guest(guest)
-
- gitlab_sign_out
- sign_in(guest)
- visit public_send("project_#{issuable_type}_path", project, issuable)
- end
-
- it "does not unlock the #{issuable_type} discussion" do
- add_note("/unlock")
-
- expect(page).not_to have_content 'Commands applied'
-
- expect(issuable).to be_discussion_locked
- end
- end
- end
- end
-
- describe "preview of note on #{issuable_type}", :js do
- it 'removes quick actions from note and explains them' do
- create(:user, username: 'bob')
-
- visit public_send("project_#{issuable_type}_path", project, issuable)
-
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "Awesome!\n/assign @bob "
- click_on 'Preview'
-
- expect(page).to have_content 'Awesome!'
- expect(page).not_to have_content '/assign @bob'
- expect(page).to have_content 'Assigns @bob.'
- end
- end
- end
-end
diff --git a/spec/support/features/reportable_note_shared_examples.rb b/spec/support/features/reportable_note_shared_examples.rb
index 8cfce49da8a..5d5a0a7b5d2 100644
--- a/spec/support/features/reportable_note_shared_examples.rb
+++ b/spec/support/features/reportable_note_shared_examples.rb
@@ -20,7 +20,7 @@ shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- expect(dropdown).to have_link('Report abuse to GitLab', href: abuse_report_path)
+ expect(dropdown).to have_link('Report abuse to admin', href: abuse_report_path)
if type == 'issue' || type == 'merge_request'
expect(dropdown).to have_button('Delete comment')
@@ -33,7 +33,7 @@ shared_examples 'reportable note' do |type|
dropdown = comment.find(more_actions_selector)
open_dropdown(dropdown)
- dropdown.click_link('Report abuse to GitLab')
+ dropdown.click_link('Report abuse to admin')
expect(find('#user_name')['value']).to match(note.author.username)
expect(find('#abuse_report_message')['value']).to match(noteable_note_url(note))
@@ -41,7 +41,7 @@ shared_examples 'reportable note' do |type|
def open_dropdown(dropdown)
# make window wide enough that tooltip doesn't trigger horizontal scrollbar
- resize_window(1200, 800)
+ restore_window_size
dropdown.find('.more-actions-toggle').click
dropdown.find('.dropdown-menu li', match: :first)
diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb
index 0a464d77cb7..01531864c1f 100644
--- a/spec/support/features/variable_list_shared_examples.rb
+++ b/spec/support/features/variable_list_shared_examples.rb
@@ -8,7 +8,7 @@ shared_examples 'variable list' do
it 'adds new CI variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
- find('.js-ci-variable-input-value').set('key value')
+ find('.js-ci-variable-input-value').set('key_value')
end
click_button('Save variables')
@@ -17,16 +17,19 @@ shared_examples 'variable list' do
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
- page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
- expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key value')
+ expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
end
end
- it 'adds empty variable' do
+ it 'adds a new protected variable' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
- find('.js-ci-variable-input-value').set('')
+ find('.js-ci-variable-input-value').set('key_value')
+ find('.ci-variable-protected-item .js-project-feature-toggle').click
+
+ expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
click_button('Save variables')
@@ -35,19 +38,19 @@ shared_examples 'variable list' do
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
- page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
- expect(find('.js-ci-variable-input-value', visible: false).value).to eq('')
+ expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
+ expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
end
end
- it 'adds new protected variable' do
+ it 'defaults to unmasked' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('key')
- find('.js-ci-variable-input-value').set('key value')
- find('.ci-variable-protected-item .js-project-feature-toggle').click
+ find('.js-ci-variable-input-value').set('key_value')
- expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
end
click_button('Save variables')
@@ -56,10 +59,10 @@ shared_examples 'variable list' do
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
- page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-key').value).to eq('key')
- expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key value')
- expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
+ expect(find('.js-ci-variable-input-value', visible: false).value).to eq('key_value')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
end
end
@@ -113,19 +116,19 @@ shared_examples 'variable list' do
page.within('.js-ci-variable-list-section') do
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value)
- expect(page).to have_content('*' * 20)
+ expect(page).to have_content('*' * 17)
click_button('Reveal value')
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value').value).to eq(variable.value)
- expect(page).not_to have_content('*' * 20)
+ expect(page).not_to have_content('*' * 17)
click_button('Hide value')
expect(first('.js-ci-variable-input-key').value).to eq(variable.key)
expect(first('.js-ci-variable-input-value', visible: false).value).to eq(variable.value)
- expect(page).to have_content('*' * 20)
+ expect(page).to have_content('*' * 17)
end
end
@@ -146,7 +149,7 @@ shared_examples 'variable list' do
page.within('.js-ci-variable-list-section') do
click_button('Reveal value')
- page.within('.js-row:nth-child(1)') do
+ page.within('.js-row:nth-child(2)') do
find('.js-ci-variable-input-key').set('new_key')
find('.js-ci-variable-input-value').set('new_value')
end
@@ -156,34 +159,13 @@ shared_examples 'variable list' do
visit page_path
- page.within('.js-row:nth-child(1)') do
+ page.within('.js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-key').value).to eq('new_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('new_value')
end
end
end
- it 'edits variable with empty value' do
- page.within('.js-ci-variable-list-section') do
- click_button('Reveal value')
-
- page.within('.js-row:nth-child(1)') do
- find('.js-ci-variable-input-key').set('new_key')
- find('.js-ci-variable-input-value').set('')
- end
-
- click_button('Save variables')
- wait_for_requests
-
- visit page_path
-
- page.within('.js-row:nth-child(1)') do
- expect(find('.js-ci-variable-input-key').value).to eq('new_key')
- expect(find('.js-ci-variable-input-value', visible: false).value).to eq('')
- end
- end
- end
-
it 'edits variable to be protected' do
# Create the unprotected variable
page.within('.js-ci-variable-list-section .js-row:last-child') do
@@ -199,7 +181,7 @@ shared_examples 'variable list' do
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
- page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
@@ -211,7 +193,7 @@ shared_examples 'variable list' do
visit page_path
# We check the first row because it re-sorts to alphabetical order on refresh
- page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(3)') do
expect(find('.js-ci-variable-input-key').value).to eq('unprotected_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('unprotected_value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('true')
@@ -233,7 +215,7 @@ shared_examples 'variable list' do
visit page_path
- page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
find('.ci-variable-protected-item .js-project-feature-toggle').click
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
@@ -244,13 +226,68 @@ shared_examples 'variable list' do
visit page_path
- page.within('.js-ci-variable-list-section .js-row:nth-child(1)') do
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
expect(find('.js-ci-variable-input-key').value).to eq('protected_key')
expect(find('.js-ci-variable-input-value', visible: false).value).to eq('protected_value')
expect(find('.js-ci-variable-input-protected', visible: false).value).to eq('false')
end
end
+ it 'edits variable to be unmasked' do
+ page.within('.js-ci-variable-list-section .js-row:last-child') do
+ find('.js-ci-variable-input-key').set('unmasked_key')
+ find('.js-ci-variable-input-value').set('unmasked_value')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
+
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ visit page_path
+
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
+
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ visit page_path
+
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+ end
+ end
+
+ it 'edits variable to be masked' do
+ page.within('.js-ci-variable-list-section .js-row:last-child') do
+ find('.js-ci-variable-input-key').set('masked_key')
+ find('.js-ci-variable-input-value').set('masked_value')
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('false')
+
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
+
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ visit page_path
+
+ page.within('.js-ci-variable-list-section .js-row:nth-child(2)') do
+ expect(find('.js-ci-variable-input-masked', visible: false).value).to eq('true')
+ end
+ end
+
it 'handles multiple edits and deletion in the middle' do
page.within('.js-ci-variable-list-section') do
# Create 2 variables
@@ -269,7 +306,7 @@ shared_examples 'variable list' do
expect(page).to have_selector('.js-row', count: 4)
# Remove the `akey` variable
- page.within('.js-row:nth-child(2)') do
+ page.within('.js-row:nth-child(3)') do
first('.js-row-remove-button').click
end
@@ -297,11 +334,11 @@ shared_examples 'variable list' do
it 'shows validation error box about duplicate keys' do
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('samekey')
- find('.js-ci-variable-input-value').set('value1')
+ find('.js-ci-variable-input-value').set('value123')
end
page.within('.js-ci-variable-list-section .js-row:last-child') do
find('.js-ci-variable-input-key').set('samekey')
- find('.js-ci-variable-input-value').set('value2')
+ find('.js-ci-variable-input-value').set('value456')
end
click_button('Save variables')
@@ -314,4 +351,36 @@ shared_examples 'variable list' do
expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/)
end
end
+
+ it 'shows validation error box about masking empty values' do
+ page.within('.js-ci-variable-list-section .js-row:last-child') do
+ find('.js-ci-variable-input-key').set('empty_value')
+ find('.js-ci-variable-input-value').set('')
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ page.within('.js-ci-variable-list-section') do
+ expect(all('.js-ci-variable-error-box ul li').count).to eq(1)
+ expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/)
+ end
+ end
+
+ it 'shows validation error box about unmaskable values' do
+ page.within('.js-ci-variable-list-section .js-row:last-child') do
+ find('.js-ci-variable-input-key').set('unmaskable_value')
+ find('.js-ci-variable-input-value').set('???')
+ find('.ci-variable-masked-item .js-project-feature-toggle').click
+ end
+
+ click_button('Save variables')
+ wait_for_requests
+
+ page.within('.js-ci-variable-list-section') do
+ expect(all('.js-ci-variable-error-box ul li').count).to eq(1)
+ expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables value is invalid/)
+ end
+ end
end
diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb
index ecefdc23811..33648292037 100644
--- a/spec/support/helpers/cycle_analytics_helpers.rb
+++ b/spec/support/helpers/cycle_analytics_helpers.rb
@@ -23,7 +23,7 @@ module CycleAnalyticsHelpers
return if skip_push_handler
- GitPushService.new(project,
+ Git::BranchPushService.new(project,
user,
oldrev: oldrev,
newrev: commit_shas.last,
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index ad6e1064499..ed049daba80 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -16,7 +16,9 @@ module EmailHelpers
end
def should_email(user, times: 1, recipients: email_recipients)
- expect(sent_to_user(user, recipients: recipients)).to eq(times)
+ amount = sent_to_user(user, recipients: recipients)
+ failed_message = lambda { "User #{user.username} (#{user.id}): email test failed (expected #{times}, got #{amount})" }
+ expect(amount).to eq(times), failed_message
end
def should_not_email(user, recipients: email_recipients)
diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb
index 89517fde6e2..8a139fafac2 100644
--- a/spec/support/helpers/features/notes_helpers.rb
+++ b/spec/support/helpers/features/notes_helpers.rb
@@ -23,8 +23,16 @@ module Spec
def preview_note(text)
page.within('.js-main-target-form') do
- fill_in('note[note]', with: text)
+ filled_text = fill_in('note[note]', with: text)
+
+ # Wait for quick action prompt to load and then dismiss it with ESC
+ # because it may block the Preview button
+ wait_for_requests
+ filled_text.send_keys(:escape)
+
click_on('Preview')
+
+ yield if block_given?
end
end
end
diff --git a/spec/support/helpers/file_mover_helpers.rb b/spec/support/helpers/file_mover_helpers.rb
new file mode 100644
index 00000000000..1ba7cc03354
--- /dev/null
+++ b/spec/support/helpers/file_mover_helpers.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module FileMoverHelpers
+ def stub_file_mover(file_path, stub_real_path: nil)
+ file_name = File.basename(file_path)
+ allow(Pathname).to receive(:new).and_call_original
+
+ expect_next_instance_of(Pathname, a_string_including(file_name)) do |pathname|
+ allow(pathname).to receive(:realpath) { stub_real_path || pathname.cleanpath }
+ end
+ end
+end
diff --git a/spec/support/helpers/filtered_search_helpers.rb b/spec/support/helpers/filtered_search_helpers.rb
index 6569feec39b..34ef185ea27 100644
--- a/spec/support/helpers/filtered_search_helpers.rb
+++ b/spec/support/helpers/filtered_search_helpers.rb
@@ -78,20 +78,17 @@ module FilteredSearchHelpers
# .tokens-container to make sure the correct names and values are rendered
def expect_tokens(tokens)
page.within '.filtered-search-box .tokens-container' do
- page.all(:css, '.tokens-container li .selectable').each_with_index do |el, index|
- token_name = tokens[index][:name]
- token_value = tokens[index][:value]
- token_emoji = tokens[index][:emoji_name]
+ token_elements = page.all(:css, 'li.filtered-search-token')
- expect(el.find('.name')).to have_content(token_name)
+ tokens.each_with_index do |token, index|
+ el = token_elements[index]
- if token_value
- expect(el.find('.value')).to have_content(token_value)
- end
+ expect(el.find('.name')).to have_content(token[:name])
+ expect(el.find('.value')).to have_content(token[:value]) if token[:value].present?
# gl-emoji content is blank when the emoji unicode is not supported
- if token_emoji
- selector = %(gl-emoji[data-name="#{token_emoji}"])
+ if token[:emoji_name].present?
+ selector = %(gl-emoji[data-name="#{token[:emoji_name]}"])
expect(el.find('.value')).to have_css(selector)
end
end
@@ -149,4 +146,10 @@ module FilteredSearchHelpers
loop until find('.filtered-search').value.strip == text
end
end
+
+ def close_dropdown_menu_if_visible
+ find('.dropdown-menu-toggle', visible: :all).tap do |toggle|
+ toggle.click if toggle.visible?
+ end
+ end
end
diff --git a/spec/support/helpers/git_helpers.rb b/spec/support/helpers/git_helpers.rb
index 99a7c39852e..99c5871ba54 100644
--- a/spec/support/helpers/git_helpers.rb
+++ b/spec/support/helpers/git_helpers.rb
@@ -6,12 +6,4 @@ module GitHelpers
Rugged::Repository.new(path)
end
-
- def project_hook_exists?(project)
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- project_path = project.repository.raw_repository.path
-
- File.exist?(File.join(project_path, 'hooks', 'post-receive'))
- end
- end
end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index e468ee4676d..e95c7f2a6d6 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -18,12 +18,10 @@ module GraphqlHelpers
# Runs a block inside a BatchLoader::Executor wrapper
def batch(max_queries: nil, &blk)
wrapper = proc do
- begin
- BatchLoader::Executor.ensure_current
- yield
- ensure
- BatchLoader::Executor.clear_current
- end
+ BatchLoader::Executor.ensure_current
+ yield
+ ensure
+ BatchLoader::Executor.clear_current
end
if max_queries
@@ -63,7 +61,14 @@ module GraphqlHelpers
def variables_for_mutation(name, input)
graphql_input = input.map { |name, value| [GraphqlHelpers.fieldnamerize(name), value] }.to_h
- { input_variable_name_for_mutation(name) => graphql_input }.to_json
+ result = { input_variable_name_for_mutation(name) => graphql_input }
+
+ # Avoid trying to serialize multipart data into JSON
+ if graphql_input.values.none? { |value| io_value?(value) }
+ result.to_json
+ else
+ result
+ end
end
def input_variable_name_for_mutation(mutation_name)
@@ -77,14 +82,28 @@ module GraphqlHelpers
def query_graphql_field(name, attributes = {}, fields = nil)
fields ||= all_graphql_fields_for(name.classify)
attributes = attributes_to_graphql(attributes)
+ attributes = "(#{attributes})" if attributes.present?
<<~QUERY
- #{name}(#{attributes}) {
- #{fields}
- }
+ #{name}#{attributes}
+ #{wrap_fields(fields)}
QUERY
end
+ def wrap_fields(fields)
+ fields = Array.wrap(fields).join("\n")
+ return unless fields.present?
+
+ <<~FIELDS
+ {
+ #{fields}
+ }
+ FIELDS
+ end
+
def all_graphql_fields_for(class_name, parent_types = Set.new)
+ allow_unlimited_graphql_complexity
+ allow_unlimited_graphql_depth
+
type = GitlabSchema.types[class_name.to_s]
return "" unless type
@@ -115,8 +134,12 @@ module GraphqlHelpers
end.join(", ")
end
- def post_graphql(query, current_user: nil, variables: nil)
- post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }
+ def post_multiplex(queries, current_user: nil, headers: {})
+ post api('/', current_user, version: 'graphql'), params: { _json: queries }, headers: headers
+ end
+
+ def post_graphql(query, current_user: nil, variables: nil, headers: {})
+ post api('/', current_user, version: 'graphql'), params: { query: query, variables: variables }, headers: headers
end
def post_graphql_mutation(mutation, current_user: nil)
@@ -128,7 +151,14 @@ module GraphqlHelpers
end
def graphql_errors
- json_response['errors']
+ case json_response
+ when Hash # regular query
+ json_response['errors']
+ when Array # multiplexed queries
+ json_response.map { |response| response['errors'] }
+ else
+ raise "Unkown GraphQL response type #{json_response.class}"
+ end
end
def graphql_mutation_response(mutation_name)
@@ -151,6 +181,10 @@ module GraphqlHelpers
field.arguments.values.any? { |argument| argument.type.non_null? }
end
+ def io_value?(value)
+ Array.wrap(value).any? { |v| v.respond_to?(:to_io) }
+ end
+
def field_type(field)
field_type = field.type
@@ -162,4 +196,19 @@ module GraphqlHelpers
field_type
end
+
+ # for most tests, we want to allow unlimited complexity
+ def allow_unlimited_graphql_complexity
+ allow_any_instance_of(GitlabSchema).to receive(:max_complexity).and_return nil
+ allow(GitlabSchema).to receive(:max_query_complexity).with(any_args).and_return nil
+ end
+
+ def allow_unlimited_graphql_depth
+ allow_any_instance_of(GitlabSchema).to receive(:max_depth).and_return nil
+ allow(GitlabSchema).to receive(:max_query_depth).with(any_args).and_return nil
+ end
end
+
+# This warms our schema, doing this as part of loading the helpers to avoid
+# duplicate loading error when Rails tries autoload the types.
+GitlabSchema.graphql_definition
diff --git a/spec/support/helpers/javascript_fixtures_helpers.rb b/spec/support/helpers/javascript_fixtures_helpers.rb
index 89c5ec7a718..cdd7724cc13 100644
--- a/spec/support/helpers/javascript_fixtures_helpers.rb
+++ b/spec/support/helpers/javascript_fixtures_helpers.rb
@@ -2,47 +2,56 @@ require 'action_dispatch/testing/test_request'
require 'fileutils'
module JavaScriptFixturesHelpers
+ extend ActiveSupport::Concern
include Gitlab::Popen
- FIXTURE_PATH = 'spec/javascripts/fixtures'.freeze
+ extend self
- def self.included(base)
+ included do |base|
base.around do |example|
# pick an arbitrary date from the past, so tests are not time dependent
Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run }
+
+ raise NoMethodError.new('You need to set `response` for the fixture generator! This will automatically happen with `type: :controller` or `type: :request`.', 'response') unless respond_to?(:response)
+
+ store_frontend_fixture(response, example.description)
end
end
+ def fixture_root_path
+ (Gitlab.ee? ? 'ee/' : '') + 'spec/javascripts/fixtures'
+ end
+
# Public: Removes all fixture files from given directory
#
- # directory_name - directory of the fixtures (relative to FIXTURE_PATH)
+ # directory_name - directory of the fixtures (relative to .fixture_root_path)
#
def clean_frontend_fixtures(directory_name)
- directory_name = File.expand_path(directory_name, FIXTURE_PATH)
- Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name|
+ full_directory_name = File.expand_path(directory_name, fixture_root_path)
+ Dir[File.expand_path('*.html', full_directory_name)].each do |file_name|
FileUtils.rm(file_name)
end
end
- # Public: Store a response object as fixture file
+ def remove_repository(project)
+ Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
+ end
+
+ private
+
+ # Private: Store a response object as fixture file
#
# response - string or response object to store
- # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
+ # fixture_file_name - file name to store the fixture in (relative to .fixture_root_path)
#
def store_frontend_fixture(response, fixture_file_name)
- fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
+ full_fixture_path = File.expand_path(fixture_file_name, fixture_root_path)
fixture = response.respond_to?(:body) ? parse_response(response) : response
- FileUtils.mkdir_p(File.dirname(fixture_file_name))
- File.write(fixture_file_name, fixture)
- end
-
- def remove_repository(project)
- Gitlab::Shell.new.remove_repository(project.repository_storage, project.disk_path)
+ FileUtils.mkdir_p(File.dirname(full_fixture_path))
+ File.write(full_fixture_path, fixture)
end
- private
-
# Private: Prepare a response object for use as a frontend fixture
#
# response - response object to prepare
diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb
index 9dc89b483b2..011c4df0fe5 100644
--- a/spec/support/helpers/kubernetes_helpers.rb
+++ b/spec/support/helpers/kubernetes_helpers.rb
@@ -9,47 +9,86 @@ module KubernetesHelpers
kube_response(kube_pods_body)
end
+ def kube_logs_response
+ kube_response(kube_logs_body)
+ end
+
def kube_deployments_response
kube_response(kube_deployments_body)
end
- def stub_kubeclient_discover(api_url)
+ def stub_kubeclient_discover_base(api_url)
WebMock.stub_request(:get, api_url + '/api/v1').to_return(kube_response(kube_v1_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/extensions/v1beta1').to_return(kube_response(kube_v1beta1_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1').to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
- WebMock.stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1').to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/extensions/v1beta1')
+ .to_return(kube_response(kube_v1beta1_discovery_body))
+ WebMock
+ .stub_request(:get, api_url + '/apis/rbac.authorization.k8s.io/v1')
+ .to_return(kube_response(kube_v1_rbac_authorization_discovery_body))
+ end
+
+ def stub_kubeclient_discover(api_url)
+ stub_kubeclient_discover_base(api_url)
+
+ WebMock
+ .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+ .to_return(kube_response(kube_v1alpha1_serving_knative_discovery_body))
+ end
+
+ def stub_kubeclient_discover_knative_not_found(api_url)
+ stub_kubeclient_discover_base(api_url)
+
+ WebMock
+ .stub_request(:get, api_url + '/apis/serving.knative.dev/v1alpha1')
+ .to_return(status: [404, "Resource Not Found"])
end
- def stub_kubeclient_service_pods(response = nil)
+ def stub_kubeclient_service_pods(response = nil, options = {})
stub_kubeclient_discover(service.api_url)
- pods_url = service.api_url + "/api/v1/pods"
+
+ namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
+ pods_url = service.api_url + "/api/v1/#{namespace_path}pods"
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
- def stub_kubeclient_pods(response = nil)
+ def stub_kubeclient_pods(namespace, status: nil)
stub_kubeclient_discover(service.api_url)
- pods_url = service.api_url + "/api/v1/namespaces/#{service.actual_namespace}/pods"
+ pods_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods"
+ response = { status: status } if status
WebMock.stub_request(:get, pods_url).to_return(response || kube_pods_response)
end
- def stub_kubeclient_deployments(response = nil)
+ def stub_kubeclient_logs(pod_name, namespace, status: nil)
stub_kubeclient_discover(service.api_url)
- deployments_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{service.actual_namespace}/deployments"
+ logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}/log?tailLines=#{Clusters::Platforms::Kubernetes::LOGS_LIMIT}"
+ response = { status: status } if status
+
+ WebMock.stub_request(:get, logs_url).to_return(response || kube_logs_response)
+ end
+
+ def stub_kubeclient_deployments(namespace, status: nil)
+ stub_kubeclient_discover(service.api_url)
+ deployments_url = service.api_url + "/apis/extensions/v1beta1/namespaces/#{namespace}/deployments"
+ response = { status: status } if status
WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response)
end
- def stub_kubeclient_knative_services(**options)
+ def stub_kubeclient_knative_services(options = {})
+ namespace_path = options[:namespace].present? ? "namespaces/#{options[:namespace]}/" : ""
+
options[:name] ||= "kubetest"
- options[:namespace] ||= "default"
options[:domain] ||= "example.com"
+ options[:response] ||= kube_response(kube_knative_services_body(options))
stub_kubeclient_discover(service.api_url)
- knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/services"
- WebMock.stub_request(:get, knative_url).to_return(kube_response(kube_knative_services_body(options)))
+ knative_url = service.api_url + "/apis/serving.knative.dev/v1alpha1/#{namespace_path}services"
+
+ WebMock.stub_request(:get, knative_url).to_return(options[:response])
end
def stub_kubeclient_get_secret(api_url, **options)
@@ -212,6 +251,10 @@ module KubernetesHelpers
}
end
+ def kube_logs_body
+ "Log 1\nLog 2\nLog 3"
+ end
+
def kube_deployments_body
{
"kind" => "DeploymentList",
@@ -235,16 +278,20 @@ module KubernetesHelpers
# This is a partial response, it will have many more elements in reality but
# these are the ones we care about at the moment
- def kube_pod(name: "kube-pod", app: "valid-pod-label", status: "Running", track: nil)
+ def kube_pod(name: "kube-pod", environment_slug: "production", namespace: "project-namespace", project_slug: "project-path-slug", status: "Running", track: nil)
{
"metadata" => {
"name" => name,
+ "namespace" => namespace,
"generate_name" => "generated-name-with-suffix",
"creationTimestamp" => "2016-11-25T19:55:19Z",
+ "annotations" => {
+ "app.gitlab.com/env" => environment_slug,
+ "app.gitlab.com/app" => project_slug
+ },
"labels" => {
- "app" => app,
"track" => track
- }
+ }.compact
},
"spec" => {
"containers" => [
@@ -278,13 +325,16 @@ module KubernetesHelpers
}
end
- def kube_deployment(name: "kube-deployment", app: "valid-deployment-label", track: nil)
+ def kube_deployment(name: "kube-deployment", environment_slug: "production", project_slug: "project-path-slug", track: nil)
{
"metadata" => {
"name" => name,
"generation" => 4,
+ "annotations" => {
+ "app.gitlab.com/env" => environment_slug,
+ "app.gitlab.com/app" => project_slug
+ },
"labels" => {
- "app" => app,
"track" => track
}.compact
},
@@ -348,12 +398,13 @@ module KubernetesHelpers
def kube_terminals(service, pod)
pod_name = pod['metadata']['name']
+ pod_namespace = pod['metadata']['namespace']
containers = pod['spec']['containers']
containers.map do |container|
terminal = {
selectors: { pod: pod_name, container: container['name'] },
- url: container_exec_url(service.api_url, service.actual_namespace, pod_name, container['name']),
+ url: container_exec_url(service.api_url, pod_namespace, pod_name, container['name']),
subprotocols: ['channel.k8s.io'],
headers: { 'Authorization' => ["Bearer #{service.token}"] },
created_at: DateTime.parse(pod['metadata']['creationTimestamp']),
diff --git a/spec/support/helpers/lets_encrypt_helpers.rb b/spec/support/helpers/lets_encrypt_helpers.rb
new file mode 100644
index 00000000000..2857416ad95
--- /dev/null
+++ b/spec/support/helpers/lets_encrypt_helpers.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module LetsEncryptHelpers
+ ACME_ORDER_METHODS = {
+ url: 'https://example.com/',
+ status: 'valid',
+ expires: 2.days.from_now
+ }.freeze
+
+ ACME_CHALLENGE_METHODS = {
+ status: 'pending',
+ token: 'tokenvalue',
+ file_content: 'hereisfilecontent',
+ request_validation: true
+ }.freeze
+
+ def stub_lets_encrypt_settings
+ stub_application_setting(
+ lets_encrypt_notification_email: 'myemail@test.example.com',
+ lets_encrypt_terms_of_service_accepted: true
+ )
+ end
+
+ def stub_lets_encrypt_client
+ client = instance_double('Acme::Client')
+
+ allow(client).to receive(:new_account)
+ allow(client).to receive(:terms_of_service).and_return(
+ "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf"
+ )
+
+ allow(Acme::Client).to receive(:new).with(
+ private_key: kind_of(OpenSSL::PKey::RSA),
+ directory: ::Gitlab::LetsEncrypt::Client::STAGING_DIRECTORY_URL
+ ).and_return(client)
+
+ client
+ end
+
+ def acme_challenge_double
+ challenge = instance_double('Acme::Client::Resources::Challenges::HTTP01')
+ allow(challenge).to receive_messages(ACME_CHALLENGE_METHODS)
+ challenge
+ end
+
+ def acme_authorization_double
+ authorization = instance_double('Acme::Client::Resources::Authorization')
+ allow(authorization).to receive(:http).and_return(acme_challenge_double)
+ authorization
+ end
+
+ def acme_order_double(attributes = {})
+ acme_order = instance_double('Acme::Client::Resources::Order')
+ allow(acme_order).to receive_messages(ACME_ORDER_METHODS.merge(attributes))
+ allow(acme_order).to receive(:authorizations).and_return([acme_authorization_double])
+ allow(acme_order).to receive(:finalize)
+ acme_order
+ end
+end
diff --git a/spec/support/helpers/license_helper.rb b/spec/support/helpers/license_helper.rb
new file mode 100644
index 00000000000..4aaad55a8ef
--- /dev/null
+++ b/spec/support/helpers/license_helper.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+# Placeholder module for EE implementation needed for CE specs to be run in EE codebase
+module LicenseHelpers
+ def stub_licensed_features(features)
+ # do nothing
+ end
+end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 3fee6872498..0bb2d2510c2 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -47,7 +47,7 @@ module LoginHelpers
end
def gitlab_sign_in_via(provider, user, uid, saml_response = nil)
- mock_auth_hash(provider, uid, user.email, saml_response)
+ mock_auth_hash_with_saml_xml(provider, uid, user.email, saml_response)
visit new_user_session_path
click_link provider
end
@@ -87,7 +87,12 @@ module LoginHelpers
click_link "oauth-login-#{provider}"
end
- def mock_auth_hash(provider, uid, email, saml_response = nil)
+ def mock_auth_hash_with_saml_xml(provider, uid, email, saml_response)
+ response_object = { document: saml_xml(saml_response) }
+ mock_auth_hash(provider, uid, email, response_object: response_object)
+ end
+
+ def mock_auth_hash(provider, uid, email, response_object: nil)
# The mock_auth configuration allows you to set per-provider (or default)
# authentication hashes to return during integration testing.
OmniAuth.config.mock_auth[provider.to_sym] = OmniAuth::AuthHash.new({
@@ -110,12 +115,13 @@ module LoginHelpers
image: 'mock_user_thumbnail_url'
}
},
- response_object: {
- document: saml_xml(saml_response)
- }
+ response_object: response_object
}
})
+ original_env_config_omniauth_auth = Rails.application.env_config['omniauth.auth']
Rails.application.env_config['omniauth.auth'] = OmniAuth.config.mock_auth[provider.to_sym]
+
+ original_env_config_omniauth_auth
end
def saml_xml(raw_saml_response)
diff --git a/spec/support/helpers/metrics_dashboard_helpers.rb b/spec/support/helpers/metrics_dashboard_helpers.rb
new file mode 100644
index 00000000000..1f36b0e217c
--- /dev/null
+++ b/spec/support/helpers/metrics_dashboard_helpers.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module MetricsDashboardHelpers
+ def project_with_dashboard(dashboard_path, dashboard_yml = nil)
+ dashboard_yml ||= fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')
+
+ create(:project, :custom_repo, files: { dashboard_path => dashboard_yml })
+ end
+
+ def delete_project_dashboard(project, user, dashboard_path)
+ project.repository.delete_file(
+ user,
+ dashboard_path,
+ branch_name: 'master',
+ message: 'Delete dashboard'
+ )
+
+ project.repository.refresh_method_caches([:metrics_dashboard])
+ end
+
+ shared_examples_for 'misconfigured dashboard service response' do |status_code|
+ it 'returns an appropriate message and status code' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:message, :http_status, :status)
+ expect(result[:status]).to eq(:error)
+ expect(result[:http_status]).to eq(status_code)
+ end
+ end
+
+ shared_examples_for 'valid dashboard service response' do
+ let(:dashboard_schema) { JSON.parse(fixture_file('lib/gitlab/metrics/dashboard/schemas/dashboard.json')) }
+
+ it 'returns a json representation of the dashboard' do
+ result = service_call
+
+ expect(result.keys).to contain_exactly(:dashboard, :status)
+ expect(result[:status]).to eq(:success)
+
+ expect(JSON::Validator.fully_validate(dashboard_schema, result[:dashboard])).to be_empty
+ end
+ end
+end
diff --git a/spec/support/helpers/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb
index 9dc1f1de436..4230d315d9b 100644
--- a/spec/support/helpers/mobile_helpers.rb
+++ b/spec/support/helpers/mobile_helpers.rb
@@ -8,7 +8,7 @@ module MobileHelpers
end
def restore_window_size
- resize_window(1366, 768)
+ resize_window(*CAPYBARA_WINDOW_SIZE)
end
def resize_window(width, height)
diff --git a/spec/support/helpers/notification_helpers.rb b/spec/support/helpers/notification_helpers.rb
index 8d84510fb73..44c2051598c 100644
--- a/spec/support/helpers/notification_helpers.rb
+++ b/spec/support/helpers/notification_helpers.rb
@@ -17,11 +17,15 @@ module NotificationHelpers
def create_user_with_notification(level, username, resource = project)
user = create(:user, username: username)
+ create_notification_setting(user, resource, level)
+
+ user
+ end
+
+ def create_notification_setting(user, resource, level)
setting = user.notification_settings_for(resource)
setting.level = level
setting.save
-
- user
end
# Create custom notifications
diff --git a/spec/support/helpers/policy_helpers.rb b/spec/support/helpers/policy_helpers.rb
new file mode 100644
index 00000000000..3d780eb5fb1
--- /dev/null
+++ b/spec/support/helpers/policy_helpers.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module PolicyHelpers
+ def expect_allowed(*permissions)
+ permissions.each { |p| is_expected.to be_allowed(p) }
+ end
+
+ def expect_disallowed(*permissions)
+ permissions.each { |p| is_expected.not_to be_allowed(p) }
+ end
+end
diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb
index 9a86560da2a..bcb11a09b36 100644
--- a/spec/support/helpers/project_forks_helper.rb
+++ b/spec/support/helpers/project_forks_helper.rb
@@ -1,5 +1,11 @@
module ProjectForksHelper
def fork_project(project, user = nil, params = {})
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ fork_project_direct(project, user, params)
+ end
+ end
+
+ def fork_project_direct(project, user = nil, params = {})
# Load the `fork_network` for the project to fork as there might be one that
# wasn't loaded yet.
project.reload unless project.fork_network
@@ -44,11 +50,16 @@ module ProjectForksHelper
end
def fork_project_with_submodules(project, user = nil, params = {})
- forked_project = fork_project(project, user, params)
- TestEnv.copy_repo(forked_project,
- bare_repo: TestEnv.forked_repo_path_bare,
- refs: TestEnv::FORKED_BRANCH_SHA)
- forked_project.repository.after_import
- forked_project
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ forked_project = fork_project_direct(project, user, params)
+ TestEnv.copy_repo(
+ forked_project,
+ bare_repo: TestEnv.forked_repo_path_bare,
+ refs: TestEnv::FORKED_BRANCH_SHA
+ )
+ forked_project.repository.after_import
+
+ forked_project
+ end
end
end
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index ce1f9fce10d..87f825152cf 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -7,6 +7,10 @@ module PrometheusHelpers
%{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100}
end
+ def prometheus_istio_query(function_name, kube_namespace)
+ %{floor(sum(rate(istio_revision_request_count{destination_configuration=\"#{function_name}\", destination_namespace=\"#{kube_namespace}\"}[1m])*30))}
+ end
+
def prometheus_ping_url(prometheus_query)
query = { query: prometheus_query }.to_query
@@ -25,12 +29,16 @@ module PrometheusHelpers
"https://prometheus.example.com/api/v1/query?#{query}"
end
- def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now.to_f)
+ def prometheus_query_range_url(prometheus_query, start: 8.hours.ago, stop: Time.now, step: nil)
+ start = start.to_f
+ stop = stop.to_f
+ step ||= Gitlab::PrometheusClient.compute_step(start, stop)
+
query = {
query: prometheus_query,
- start: start.to_f,
+ start: start,
end: stop,
- step: 1.minute.to_i
+ step: step
}.to_query
"https://prometheus.example.com/api/v1/query_range?#{query}"
diff --git a/spec/support/helpers/query_recorder.rb b/spec/support/helpers/query_recorder.rb
index 7ce63375d34..c4ae62b25e4 100644
--- a/spec/support/helpers/query_recorder.rb
+++ b/spec/support/helpers/query_recorder.rb
@@ -17,7 +17,7 @@ module ActiveRecord
def callback(name, start, finish, message_id, values)
show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
- if values[:name]&.include?("CACHE") && skip_cached
+ if values[:cached] && skip_cached
@cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA")
@log << values[:sql]
diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb
index 3c6956cf5e0..44d95a029af 100644
--- a/spec/support/helpers/repo_helpers.rb
+++ b/spec/support/helpers/repo_helpers.rb
@@ -11,6 +11,8 @@ module RepoHelpers
# blob.path # => 'files/js/commit.js.coffee'
# blob.data # => 'class Commit...'
#
+ # Build the options hash that's passed to Rugged::Commit#create
+
def sample_blob
OpenStruct.new(
oid: '5f53439ca4b009096571d3c8bc3d09d30e7431b3',
@@ -115,4 +117,94 @@ eos
commits: commits
)
end
+
+ def create_file_in_repo(
+ project, start_branch, branch_name, filename, content,
+ commit_message: 'Add new content')
+ Files::CreateService.new(
+ project,
+ project.owner,
+ commit_message: commit_message,
+ start_branch: start_branch,
+ branch_name: branch_name,
+ file_path: filename,
+ file_content: content
+ ).execute
+ end
+
+ def commit_options(repo, index, target, ref, message)
+ options = {}
+ options[:tree] = index.write_tree(repo)
+ options[:author] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:committer] = {
+ email: "test@example.com",
+ name: "Test Author",
+ time: Time.gm(2014, "mar", 3, 20, 15, 1)
+ }
+ options[:message] ||= message
+ options[:parents] = repo.empty? ? [] : [target].compact
+ options[:update_ref] = ref
+
+ options
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of CHANGELOG with a single new line of text.
+ def new_commit_edit_old_file(repo)
+ oid = repo.write("I replaced the changelog with this text", :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "CHANGELOG", oid: oid, mode: 0100644)
+
+ options = commit_options(
+ repo,
+ index,
+ repo.head.target,
+ "HEAD",
+ "Edit CHANGELOG in its original location"
+ )
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of the specified file_path with new text.
+ def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head)
+ oid = repo.write(text, :blob)
+ index = repo.index
+ index.read_tree(branch.target.tree)
+ index.add(path: file_path, oid: oid, mode: 0100644)
+ options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message)
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the
+ # contents of encoding/CHANGELOG with new text.
+ def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text)
+ branch = repo.branches[branch_name]
+ new_commit_edit_new_file(repo, file_path, commit_message, text, branch)
+ end
+
+ # Writes a new commit to the repo and returns a Rugged::Commit. Moves the
+ # CHANGELOG file to the encoding/ directory.
+ def new_commit_move_file(repo)
+ blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid]
+ file_content = repo.lookup(blob_oid).content
+ oid = repo.write(file_content, :blob)
+ index = repo.index
+ index.read_tree(repo.head.target.tree)
+ index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644)
+ index.remove("CHANGELOG")
+
+ options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/")
+
+ sha = Rugged::Commit.create(repo, options)
+ repo.lookup(sha)
+ end
end
diff --git a/spec/support/helpers/select2_helper.rb b/spec/support/helpers/select2_helper.rb
index f4f0415985c..87672c8896d 100644
--- a/spec/support/helpers/select2_helper.rb
+++ b/spec/support/helpers/select2_helper.rb
@@ -35,6 +35,10 @@ module Select2Helper
execute_script("$('#{selector}').select2('open');")
end
+ def close_select2(selector)
+ execute_script("$('#{selector}').select2('close');")
+ end
+
def scroll_select2_to_bottom(selector)
evaluate_script "$('#{selector}').scrollTop($('#{selector}')[0].scrollHeight); $('#{selector}');"
end
diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb
index ff21bbe28ca..f6c613ad5aa 100644
--- a/spec/support/helpers/stub_configuration.rb
+++ b/spec/support/helpers/stub_configuration.rb
@@ -2,7 +2,8 @@ require 'active_support/core_ext/hash/transform_values'
require 'active_support/hash_with_indifferent_access'
require 'active_support/dependencies'
-require_dependency 'gitlab'
+# check gets rid of already initialized constant warnings when using spring
+require_dependency 'gitlab' unless defined?(Gitlab)
module StubConfiguration
def stub_application_setting(messages)
@@ -84,6 +85,10 @@ module StubConfiguration
allow(Gitlab.config.kerberos).to receive_messages(to_settings(messages))
end
+ def stub_gitlab_shell_setting(messages)
+ allow(Gitlab.config.gitlab_shell).to receive_messages(to_settings(messages))
+ end
+
private
# Modifies stubbed messages to also stub possible predicate versions
@@ -116,3 +121,6 @@ module StubConfiguration
end
end
end
+
+require_relative '../../../ee/spec/support/helpers/ee/stub_configuration' if
+ Dir.exist?("#{__dir__}/../../../ee")
diff --git a/spec/support/helpers/stub_object_storage.rb b/spec/support/helpers/stub_object_storage.rb
index e0c50e533a6..d31f9908714 100644
--- a/spec/support/helpers/stub_object_storage.rb
+++ b/spec/support/helpers/stub_object_storage.rb
@@ -23,15 +23,13 @@ module StubObjectStorage
Fog.mock!
::Fog::Storage.new(connection_params).tap do |connection|
- begin
- connection.directories.create(key: remote_directory)
+ connection.directories.create(key: remote_directory)
- # Cleanup remaining files
- connection.directories.each do |directory|
- directory.files.map(&:destroy)
- end
- rescue Excon::Error::Conflict
+ # Cleanup remaining files
+ connection.directories.each do |directory|
+ directory.files.map(&:destroy)
end
+ rescue Excon::Error::Conflict
end
end
@@ -75,3 +73,6 @@ module StubObjectStorage
EOS
end
end
+
+require_relative '../../../ee/spec/support/helpers/ee/stub_object_storage' if
+ Dir.exist?("#{__dir__}/../../../ee")
diff --git a/spec/support/helpers/stub_requests.rb b/spec/support/helpers/stub_requests.rb
new file mode 100644
index 00000000000..5cad35282c0
--- /dev/null
+++ b/spec/support/helpers/stub_requests.rb
@@ -0,0 +1,40 @@
+module StubRequests
+ IP_ADDRESS_STUB = '8.8.8.9'.freeze
+
+ # Fully stubs a request using WebMock class. This class also
+ # stubs the IP address the URL is translated to (DNS lookup).
+ #
+ # It expects the final request to go to the `ip_address` instead the given url.
+ # That's primarily a DNS rebind attack prevention of Gitlab::HTTP
+ # (see: Gitlab::UrlBlocker).
+ #
+ def stub_full_request(url, ip_address: IP_ADDRESS_STUB, port: 80, method: :get)
+ stub_dns(url, ip_address: ip_address, port: port)
+
+ url = stubbed_hostname(url, hostname: ip_address)
+ WebMock.stub_request(method, url)
+ end
+
+ def stub_dns(url, ip_address:, port: 80)
+ url = parse_url(url)
+ socket = Socket.sockaddr_in(port, ip_address)
+ addr = Addrinfo.new(socket)
+
+ # See Gitlab::UrlBlocker
+ allow(Addrinfo).to receive(:getaddrinfo)
+ .with(url.hostname, url.port, nil, :STREAM)
+ .and_return([addr])
+ end
+
+ def stubbed_hostname(url, hostname: IP_ADDRESS_STUB)
+ url = parse_url(url)
+ url.hostname = hostname
+ url.to_s
+ end
+
+ private
+
+ def parse_url(url)
+ url.is_a?(URI) ? url : URI(url)
+ end
+end
diff --git a/spec/support/helpers/stub_worker.rb b/spec/support/helpers/stub_worker.rb
new file mode 100644
index 00000000000..58b7ee93dff
--- /dev/null
+++ b/spec/support/helpers/stub_worker.rb
@@ -0,0 +1,9 @@
+# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
+module StubWorker
+ def stub_worker(queue:)
+ Class.new do
+ include Sidekiq::Worker
+ sidekiq_options queue: queue
+ end
+ end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 06bcf4f8013..06b5ecdf150 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -135,7 +135,7 @@ module TestEnv
def clean_gitlab_test_path
Dir[TMP_TEST_PATH].each do |entry|
- if File.basename(entry) =~ /\A(gitlab-(test|test_bare|test-fork|test-fork_bare))\z/
+ unless test_dirs.include?(File.basename(entry))
FileUtils.rm_rf(entry)
end
end
@@ -147,12 +147,15 @@ module TestEnv
version: Gitlab::Shell.version_required,
task: 'gitlab:shell:install')
- create_fake_git_hooks
+ # gitlab-shell hooks don't work in our test environment because they try to make internal API calls
+ sabotage_gitlab_shell_hooks
end
- def create_fake_git_hooks
- # gitlab-shell hooks don't work in our test environment because they try to make internal API calls
- hooks_dir = File.join(Gitlab.config.gitlab_shell.path, 'hooks')
+ def sabotage_gitlab_shell_hooks
+ create_fake_git_hooks(Gitlab::Shell.new.hooks_path)
+ end
+
+ def create_fake_git_hooks(hooks_dir)
%w[pre-receive post-receive update].each do |hook|
File.open(File.join(hooks_dir, hook), 'w', 0755) { |f| f.puts '#!/bin/sh' }
end
@@ -169,6 +172,7 @@ module TestEnv
task: "gitlab:gitaly:install[#{install_gitaly_args}]") do
Gitlab::SetupHelper.create_gitaly_configuration(gitaly_dir, { 'default' => repos_path }, force: true)
+ create_fake_git_hooks(File.join(gitaly_dir, 'ruby/git-hooks'))
start_gitaly(gitaly_dir)
end
end
@@ -198,12 +202,10 @@ module TestEnv
socket = Gitlab::GitalyClient.address('default').sub('unix:', '')
Integer(sleep_time / sleep_interval).times do
- begin
- Socket.unix(socket)
- return
- rescue
- sleep sleep_interval
- end
+ Socket.unix(socket)
+ return
+ rescue
+ sleep sleep_interval
end
raise "could not connect to gitaly at #{socket.inspect} after #{sleep_time} seconds"
@@ -310,6 +312,18 @@ module TestEnv
private
+ # These are directories that should be preserved at cleanup time
+ def test_dirs
+ @test_dirs ||= %w[
+ gitaly
+ gitlab-shell
+ gitlab-test
+ gitlab-test_bare
+ gitlab-test-fork
+ gitlab-test-fork_bare
+ ]
+ end
+
def factory_repo_path
@factory_repo_path ||= Rails.root.join('tmp', 'tests', factory_repo_name)
end
diff --git a/spec/support/helpers/test_request_helpers.rb b/spec/support/helpers/test_request_helpers.rb
index 5a84d67bdfc..39e5dafb059 100644
--- a/spec/support/helpers/test_request_helpers.rb
+++ b/spec/support/helpers/test_request_helpers.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module TestRequestHelpers
- def test_request(remote_ip: '127.0.0.1')
- ActionController::TestRequest.new({ remote_ip: remote_ip }, ActionController::TestSession.new)
+ def test_request(remote_ip: '127.0.0.1', controller: nil)
+ ActionController::TestRequest.new({ remote_ip: remote_ip }, ActionController::TestSession.new, controller)
end
end
diff --git a/spec/support/helpers/wait_for_requests.rb b/spec/support/helpers/wait_for_requests.rb
index c7f878b7371..45b9faa0fea 100644
--- a/spec/support/helpers/wait_for_requests.rb
+++ b/spec/support/helpers/wait_for_requests.rb
@@ -50,20 +50,6 @@ module WaitForRequests
finished_all_vue_resource_requests?
end
- # Waits until the passed block returns true
- def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01)
- wait_until = Time.now + max_wait_time.seconds
- loop do
- break if yield
-
- if Time.now > wait_until
- raise "Condition not met: #{condition_name}"
- else
- sleep(polling_interval)
- end
- end
- end
-
def finished_all_vue_resource_requests?
Capybara.page.evaluate_script('window.activeVueResources || 0').zero?
end
diff --git a/spec/support/helpers/wait_helpers.rb b/spec/support/helpers/wait_helpers.rb
new file mode 100644
index 00000000000..7e8e25798e8
--- /dev/null
+++ b/spec/support/helpers/wait_helpers.rb
@@ -0,0 +1,20 @@
+module WaitHelpers
+ extend self
+
+ # Waits until the passed block returns true
+ def wait_for(condition_name, max_wait_time: Capybara.default_max_wait_time, polling_interval: 0.01, reload: false)
+ wait_until = Time.now + max_wait_time.seconds
+ loop do
+ result = yield
+ break if result
+
+ page.refresh if reload
+
+ if Time.now > wait_until
+ raise "Condition not met: #{condition_name}"
+ else
+ sleep(polling_interval)
+ end
+ end
+ end
+end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index ac320934f5a..388b88f0331 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -91,7 +91,7 @@ module ExportFileHelper
loop do
object_with_parent = deep_find_with_parent(sensitive_word, project_hash)
- return nil unless object_with_parent && object_with_parent.object
+ return unless object_with_parent && object_with_parent.object
if is_safe_hash?(object_with_parent.parent, sensitive_word)
# It's in the safe list, remove hash and keep looking
diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb
index 3e4ca8b7ab0..e6899e2d23c 100644
--- a/spec/support/matchers/access_matchers.rb
+++ b/spec/support/matchers/access_matchers.rb
@@ -7,29 +7,28 @@ module AccessMatchers
extend RSpec::Matchers::DSL
include Warden::Test::Helpers
- def emulate_user(user, membership = nil)
- case user
- when :user
- login_as(create(:user))
+ def emulate_user(user_type_or_trait, membership = nil)
+ case user_type_or_trait
+ when :user, :admin
+ login_as(create(user_type_or_trait))
+ when :external, :auditor
+ login_as(create(:user, user_type_or_trait))
when :visitor
logout
- when :admin
- login_as(create(:admin))
- when :external
- login_as(create(:user, external: true))
when User
- login_as(user)
+ login_as(user_type_or_trait)
when *Gitlab::Access.sym_options_with_owner.keys
- raise ArgumentError, "cannot emulate #{user} without membership parent" unless membership
-
- role = user
+ raise ArgumentError, "cannot emulate #{user_type_or_trait} without membership parent" unless membership
- if role == :owner && membership.owner
- user = membership.owner
- else
- user = create(:user)
- membership.public_send(:"add_#{role}", user)
- end
+ role = user_type_or_trait
+ user =
+ if role == :owner && membership.owner
+ membership.owner
+ else
+ create(:user).tap do |new_user|
+ membership.public_send(:"add_#{role}", new_user)
+ end
+ end
login_as(user)
else
diff --git a/spec/support/matchers/eq_pem.rb b/spec/support/matchers/eq_pem.rb
new file mode 100644
index 00000000000..158281e4a19
--- /dev/null
+++ b/spec/support/matchers/eq_pem.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define :eq_pem do |expected_pem_string|
+ match do |actual|
+ actual.to_pem == expected_pem_string
+ end
+
+ description do
+ "contain pem #{expected_pem_string}"
+ end
+end
diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb
index 7be84838e00..7894484f590 100644
--- a/spec/support/matchers/graphql_matchers.rb
+++ b/spec/support/matchers/graphql_matchers.rb
@@ -1,8 +1,6 @@
RSpec::Matchers.define :require_graphql_authorizations do |*expected|
match do |field|
- field_definition = field.metadata[:type_class]
- expect(field_definition).to respond_to(:required_permissions)
- expect(field_definition.required_permissions).to contain_exactly(*expected)
+ expect(field.metadata[:authorize]).to eq(*expected)
end
end
diff --git a/spec/support/matchers/issuable_matchers.rb b/spec/support/matchers/issuable_matchers.rb
index f5d9a97051a..62f510b0fbd 100644
--- a/spec/support/matchers/issuable_matchers.rb
+++ b/spec/support/matchers/issuable_matchers.rb
@@ -1,4 +1,4 @@
-RSpec::Matchers.define :have_header_with_correct_id_and_link do |level, text, id, parent = ".wiki"|
+RSpec::Matchers.define :have_header_with_correct_id_and_link do |level, text, id, parent = ".md"|
match do |actual|
node = find("#{parent} h#{level} a#user-content-#{id}")
diff --git a/spec/support/matchers/not_changed_matcher.rb b/spec/support/matchers/not_changed_matcher.rb
new file mode 100644
index 00000000000..8ef4694982d
--- /dev/null
+++ b/spec/support/matchers/not_changed_matcher.rb
@@ -0,0 +1,3 @@
+# frozen_string_literal: true
+
+RSpec::Matchers.define_negated_matcher :not_change, :change
diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb
index 0fd67531c3b..8044b061ca5 100644
--- a/spec/support/prometheus/additional_metrics_shared_examples.rb
+++ b/spec/support/prometheus/additional_metrics_shared_examples.rb
@@ -46,7 +46,7 @@ RSpec.shared_examples 'additional metrics query' do
describe 'project has Kubernetes service' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
let(:environment) { create(:environment, slug: 'environment-slug', project: project) }
- let(:kube_namespace) { project.deployment_platform.actual_namespace }
+ let(:kube_namespace) { project.deployment_platform.kubernetes_namespace_for(project) }
it_behaves_like 'query context containing environment slug and filter'
diff --git a/spec/support/protected_branch_helpers.rb b/spec/support/protected_branch_helpers.rb
new file mode 100644
index 00000000000..ede16d1c1e2
--- /dev/null
+++ b/spec/support/protected_branch_helpers.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module ProtectedBranchHelpers
+ def set_allowed_to(operation, option = 'Maintainers', form: '.js-new-protected-branch')
+ within form do
+ select_elem = find(".js-allowed-to-#{operation}")
+ select_elem.click
+
+ wait_for_requests
+
+ within('.dropdown-content') do
+ Array(option).each { |opt| click_on(opt) }
+ end
+
+ # Enhanced select is used in EE, therefore an extra click is needed.
+ select_elem.click if select_elem['aria-expanded'] == 'true'
+ end
+ end
+
+ def set_protected_branch_name(branch_name)
+ find('.js-protected-branch-select').click
+ find('.dropdown-input-field').set(branch_name)
+ click_on("Create wildcard #{branch_name}")
+ end
+
+ def set_defaults
+ set_allowed_to('merge')
+ set_allowed_to('push')
+ end
+end
diff --git a/spec/support/protected_tag_helpers.rb b/spec/support/protected_tag_helpers.rb
new file mode 100644
index 00000000000..fe9be856286
--- /dev/null
+++ b/spec/support/protected_tag_helpers.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require_relative 'protected_branch_helpers'
+
+module ProtectedTagHelpers
+ include ::ProtectedBranchHelpers
+
+ def set_allowed_to(operation, option = 'Maintainers', form: '.new-protected-tag')
+ super
+ end
+
+ def set_protected_tag_name(tag_name)
+ find('.js-protected-tag-select').click
+ find('.dropdown-input-field').set(tag_name)
+ click_on("Create wildcard #{tag_name}")
+ find('.protected-tags-dropdown .dropdown-menu', visible: false)
+ end
+end
diff --git a/spec/support/redis/redis_shared_examples.rb b/spec/support/redis/redis_shared_examples.rb
index a8b00004fe7..6aa59960092 100644
--- a/spec/support/redis/redis_shared_examples.rb
+++ b/spec/support/redis/redis_shared_examples.rb
@@ -90,7 +90,7 @@ RSpec.shared_examples "redis_shared_examples" do
subject { described_class._raw_config }
let(:config_file_name) { '/var/empty/doesnotexist' }
- it 'should be frozen' do
+ it 'is frozen' do
expect(subject).to be_frozen
end
diff --git a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
index 62ae95df8c0..1284415da1f 100644
--- a/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
+++ b/spec/support/services/migrate_to_ghost_user_service_shared_examples.rb
@@ -45,7 +45,7 @@ shared_examples "migrating a deleted user's associated records to the ghost user
context "race conditions" do
context "when #{record_class_name} migration fails and is rolled back" do
before do
- expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ActiveRecord::Rollback)
end
@@ -66,7 +66,7 @@ shared_examples "migrating a deleted user's associated records to the ghost user
context "when #{record_class_name} migration fails with a non-rollback exception" do
before do
- expect_any_instance_of(record_class::ActiveRecord_Associations_CollectionProxy)
+ expect_any_instance_of(ActiveRecord::Associations::CollectionProxy)
.to receive(:update_all).and_raise(ArgumentError)
end
diff --git a/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
new file mode 100644
index 00000000000..a0d994c4d8d
--- /dev/null
+++ b/spec/support/shared_contexts/finders/group_projects_finder_shared_contexts.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+RSpec.shared_context 'GroupProjectsFinder context' do
+ let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:current_user) { create(:user) }
+ let(:options) { {} }
+
+ let(:finder) { described_class.new(group: group, current_user: current_user, options: options) }
+
+ let!(:public_project) { create(:project, :public, group: group, path: '1') }
+ let!(:private_project) { create(:project, :private, group: group, path: '2') }
+ let!(:shared_project_1) { create(:project, :public, path: '3') }
+ let!(:shared_project_2) { create(:project, :private, path: '4') }
+ let!(:shared_project_3) { create(:project, :internal, path: '5') }
+ let!(:subgroup_project) { create(:project, :public, path: '6', group: subgroup) }
+ let!(:subgroup_private_project) { create(:project, :private, path: '7', group: subgroup) }
+
+ before do
+ shared_project_1.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
+ shared_project_2.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
+ shared_project_3.project_group_links.create(group_access: Gitlab::Access::MAINTAINER, group: group)
+ end
+end
diff --git a/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb
new file mode 100644
index 00000000000..b8a9554f55f
--- /dev/null
+++ b/spec/support/shared_contexts/finders/issues_finder_shared_contexts.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+RSpec.shared_context 'IssuesFinder context' do
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+ set(:group) { create(:group) }
+ set(:subgroup) { create(:group, parent: group) }
+ set(:project1) { create(:project, group: group) }
+ set(:project2) { create(:project) }
+ set(:project3) { create(:project, group: subgroup) }
+ set(:milestone) { create(:milestone, project: project1) }
+ set(:label) { create(:label, project: project2) }
+ set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) }
+ set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) }
+ set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) }
+ set(:issue4) { create(:issue, project: project3) }
+ set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
+ set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
+ set(:award_emoji3) { create(:award_emoji, name: 'thumbsdown', user: user, awardable: issue3) }
+end
+
+RSpec.shared_context 'IssuesFinder#execute context' do
+ let!(:closed_issue) { create(:issue, author: user2, assignees: [user2], project: project2, state: 'closed') }
+ let!(:label_link) { create(:label_link, label: label, target: issue2) }
+ let(:search_user) { user }
+ let(:params) { {} }
+ let(:issues) { described_class.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
+
+ before(:context) do
+ project1.add_maintainer(user)
+ project2.add_developer(user)
+ project2.add_developer(user2)
+ project3.add_developer(user)
+
+ issue1
+ issue2
+ issue3
+ issue4
+
+ award_emoji1
+ award_emoji2
+ award_emoji3
+ end
+end
diff --git a/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
new file mode 100644
index 00000000000..ab6687f1d07
--- /dev/null
+++ b/spec/support/shared_contexts/finders/merge_requests_finder_shared_contexts.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+RSpec.shared_context 'MergeRequestsFinder multiple projects with merge requests context' do
+ include ProjectForksHelper
+
+ # We need to explicitly permit Gitaly N+1s because of the specs that use
+ # :request_store. Gitaly N+1 detection is only enabled when :request_store is,
+ # but we don't care about potential N+1s when we're just creating several
+ # projects in the setup phase.
+ def allow_gitaly_n_plus_1
+ Gitlab::GitalyClient.allow_n_plus_1_calls do
+ yield
+ end
+ end
+
+ set(:user) { create(:user) }
+ set(:user2) { create(:user) }
+
+ set(:group) { create(:group) }
+ set(:subgroup) { create(:group, parent: group) }
+ set(:project1) do
+ allow_gitaly_n_plus_1 { create(:project, :public, group: group) }
+ end
+ # We cannot use `set` here otherwise we get:
+ # Failure/Error: allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ # The use of doubles or partial doubles from rspec-mocks outside of the per-test lifecycle is not supported.
+ let(:project2) do
+ allow_gitaly_n_plus_1 do
+ fork_project(project1, user)
+ end
+ end
+ let(:project3) do
+ allow_gitaly_n_plus_1 do
+ fork_project(project1, user).tap do |project|
+ project.update!(archived: true)
+ end
+ end
+ end
+ set(:project4) do
+ allow_gitaly_n_plus_1 { create(:project, :repository, group: subgroup) }
+ end
+ set(:project5) do
+ allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
+ end
+ set(:project6) do
+ allow_gitaly_n_plus_1 { create(:project, group: subgroup) }
+ end
+
+ let!(:merge_request1) { create(:merge_request, assignees: [user], author: user, source_project: project2, target_project: project1, target_branch: 'merged-target') }
+ let!(:merge_request2) { create(:merge_request, :conflict, assignees: [user], author: user, source_project: project2, target_project: project1, state: 'closed') }
+ let!(:merge_request3) { create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, state: 'locked', title: 'thing WIP thing') }
+ let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3, title: 'WIP thing') }
+ let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4, title: '[WIP]') }
+
+ before do
+ project1.add_maintainer(user)
+ project2.add_developer(user)
+ project3.add_developer(user)
+ project4.add_developer(user)
+ project5.add_developer(user)
+ project6.add_developer(user)
+
+ project2.add_developer(user2)
+ end
+end
diff --git a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb
new file mode 100644
index 00000000000..9e1f89ee0ed
--- /dev/null
+++ b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb
@@ -0,0 +1,8 @@
+require 'spec_helper'
+
+RSpec.shared_context 'UsersFinder#execute filter by project context' do
+ set(:normal_user) { create(:user, username: 'johndoe') }
+ set(:blocked_user) { create(:user, :blocked, username: 'notsorandom') }
+ set(:external_user) { create(:user, :external) }
+ set(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') }
+end
diff --git a/spec/support/shared_contexts/merge_request_create.rb b/spec/support/shared_contexts/merge_request_create.rb
new file mode 100644
index 00000000000..529f481c2b6
--- /dev/null
+++ b/spec/support/shared_contexts/merge_request_create.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+shared_context 'merge request create context' do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { target_project }
+ let!(:milestone) { create(:milestone, project: target_project) }
+ let!(:label) { create(:label, project: target_project) }
+ let!(:label2) { create(:label, project: target_project) }
+
+ before do
+ source_project.add_maintainer(user)
+ target_project.add_maintainer(user)
+ target_project.add_maintainer(user2)
+
+ sign_in(user)
+ visit project_new_merge_request_path(target_project,
+ merge_request: {
+ source_project_id: source_project.id,
+ target_project_id: target_project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+ end
+end
diff --git a/spec/support/shared_contexts/merge_request_edit.rb b/spec/support/shared_contexts/merge_request_edit.rb
new file mode 100644
index 00000000000..c84510ff47d
--- /dev/null
+++ b/spec/support/shared_contexts/merge_request_edit.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+shared_context 'merge request edit context' do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:milestone) { create(:milestone, project: target_project) }
+ let!(:label) { create(:label, project: target_project) }
+ let!(:label2) { create(:label, project: target_project) }
+ let(:target_project) { create(:project, :public, :repository) }
+ let(:source_project) { target_project }
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: source_project,
+ target_project: target_project,
+ source_branch: 'fix',
+ target_branch: 'master')
+ end
+
+ before do
+ source_project.add_maintainer(user)
+ target_project.add_maintainer(user)
+ target_project.add_maintainer(user2)
+
+ sign_in(user)
+ visit edit_project_merge_request_path(target_project, merge_request)
+ end
+end
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
new file mode 100644
index 00000000000..b4808ac0068
--- /dev/null
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'GroupPolicy context' do
+ let(:guest) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:developer) { create(:user) }
+ let(:maintainer) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:group) { create(:group, :private) }
+
+ let(:guest_permissions) do
+ %i[
+ read_label read_group upload_file read_namespace read_group_activity
+ read_group_issues read_group_boards read_group_labels read_group_milestones
+ read_group_merge_requests
+ ]
+ end
+ let(:reporter_permissions) { [:admin_label] }
+ let(:developer_permissions) { [:admin_milestone] }
+ let(:maintainer_permissions) do
+ %i[
+ create_projects
+ read_cluster create_cluster update_cluster admin_cluster add_cluster
+ ]
+ end
+ let(:owner_permissions) do
+ [
+ :admin_group,
+ :admin_namespace,
+ :admin_group_member,
+ :change_visibility_level,
+ :set_note_created_at,
+ (Gitlab::Database.postgresql? ? :create_subgroup : nil)
+ ].compact
+ end
+
+ before do
+ group.add_guest(guest)
+ group.add_reporter(reporter)
+ group.add_developer(developer)
+ group.add_maintainer(maintainer)
+ group.add_owner(owner)
+ end
+
+ subject { described_class.new(current_user, group) }
+end
diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
new file mode 100644
index 00000000000..54d9f5b15f2
--- /dev/null
+++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'ProjectPolicy context' do
+ set(:guest) { create(:user) }
+ set(:reporter) { create(:user) }
+ set(:developer) { create(:user) }
+ set(:maintainer) { create(:user) }
+ set(:owner) { create(:user) }
+ set(:admin) { create(:admin) }
+ let(:project) { create(:project, :public, namespace: owner.namespace) }
+
+ let(:base_guest_permissions) do
+ %i[
+ read_project read_board read_list read_wiki read_issue
+ read_project_for_iids read_issue_iid read_label
+ read_milestone read_project_snippet read_project_member read_note
+ create_project create_issue create_note upload_file create_merge_request_in
+ award_emoji
+ ]
+ end
+
+ let(:base_reporter_permissions) do
+ %i[
+ download_code fork_project create_project_snippet update_issue
+ admin_issue admin_label admin_list read_commit_status read_build
+ read_container_image read_pipeline read_environment read_deployment
+ read_merge_request download_wiki_code read_sentry_issue read_prometheus
+ ]
+ end
+
+ let(:team_member_reporter_permissions) do
+ %i[build_download_code build_read_container_image]
+ end
+
+ let(:developer_permissions) do
+ %i[
+ admin_milestone admin_merge_request update_merge_request create_commit_status
+ update_commit_status create_build update_build create_pipeline
+ update_pipeline create_merge_request_from create_wiki push_code
+ resolve_note create_container_image update_container_image
+ create_environment create_deployment create_release update_release
+ ]
+ end
+
+ let(:base_maintainer_permissions) do
+ %i[
+ push_to_delete_protected_branch update_project_snippet update_environment
+ update_deployment admin_project_snippet admin_project_member admin_note admin_wiki admin_project
+ admin_commit_status admin_build admin_container_image
+ admin_pipeline admin_environment admin_deployment destroy_release add_cluster
+ daily_statistics
+ ]
+ end
+
+ let(:public_permissions) do
+ %i[
+ download_code fork_project read_commit_status read_pipeline
+ read_container_image build_download_code build_read_container_image
+ download_wiki_code read_release
+ ]
+ end
+
+ let(:base_owner_permissions) do
+ %i[
+ change_namespace change_visibility_level rename_project remove_project
+ archive_project remove_fork_project destroy_merge_request destroy_issue
+ set_issue_iid set_issue_created_at set_note_created_at
+ ]
+ end
+
+ # Used in EE specs
+ let(:additional_guest_permissions) { [] }
+ let(:additional_reporter_permissions) { [] }
+ let(:additional_maintainer_permissions) { [] }
+ let(:additional_owner_permissions) { [] }
+
+ let(:guest_permissions) { base_guest_permissions + additional_guest_permissions }
+ let(:reporter_permissions) { base_reporter_permissions + additional_reporter_permissions }
+ let(:maintainer_permissions) { base_maintainer_permissions + additional_maintainer_permissions }
+ let(:owner_permissions) { base_owner_permissions + additional_owner_permissions }
+
+ before do
+ project.add_guest(guest)
+ project.add_maintainer(maintainer)
+ project.add_developer(developer)
+ project.add_reporter(reporter)
+ end
+end
diff --git a/spec/support/shared_contexts/services_shared_context.rb b/spec/support/shared_contexts/services_shared_context.rb
index d92e8318fa0..089f1798cd2 100644
--- a/spec/support/shared_contexts/services_shared_context.rb
+++ b/spec/support/shared_contexts/services_shared_context.rb
@@ -26,6 +26,14 @@ Service.available_services_names.each do |service|
end
end
+ before do
+ if service == 'github' && respond_to?(:stub_licensed_features)
+ stub_licensed_features(github_project_service_integration: true)
+ project.clear_memoization(:disabled_services)
+ project.clear_memoization(:licensed_feature_available)
+ end
+ end
+
def initialize_service(service)
service_item = project.find_or_initialize_service(service)
service_item.properties = service_attrs
diff --git a/spec/support/shared_examples/application_setting_examples.rb b/spec/support/shared_examples/application_setting_examples.rb
new file mode 100644
index 00000000000..421303c97be
--- /dev/null
+++ b/spec/support/shared_examples/application_setting_examples.rb
@@ -0,0 +1,291 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'application settings examples' do
+ context 'restricted signup domains' do
+ it 'sets single domain' do
+ setting.domain_whitelist_raw = 'example.com'
+ expect(setting.domain_whitelist).to eq(['example.com'])
+ end
+
+ it 'sets multiple domains with spaces' do
+ setting.domain_whitelist_raw = 'example.com *.example.com'
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
+ end
+
+ it 'sets multiple domains with newlines and a space' do
+ setting.domain_whitelist_raw = "example.com\n *.example.com"
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
+ end
+
+ it 'sets multiple domains with commas' do
+ setting.domain_whitelist_raw = "example.com, *.example.com"
+ expect(setting.domain_whitelist).to eq(['example.com', '*.example.com'])
+ end
+ end
+
+ context 'blacklisted signup domains' do
+ it 'sets single domain' do
+ setting.domain_blacklist_raw = 'example.com'
+ expect(setting.domain_blacklist).to contain_exactly('example.com')
+ end
+
+ it 'sets multiple domains with spaces' do
+ setting.domain_blacklist_raw = 'example.com *.example.com'
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with newlines and a space' do
+ setting.domain_blacklist_raw = "example.com\n *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with commas' do
+ setting.domain_blacklist_raw = "example.com, *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with semicolon' do
+ setting.domain_blacklist_raw = "example.com; *.example.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com')
+ end
+
+ it 'sets multiple domains with mixture of everything' do
+ setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com"
+ expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com')
+ end
+
+ it 'sets multiple domain with file' do
+ setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt'))
+ expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar')
+ end
+ end
+
+ describe 'usage ping settings' do
+ context 'when the usage ping is disabled in gitlab.yml' do
+ before do
+ allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(false)
+ end
+
+ it 'does not allow the usage ping to be configured' do
+ expect(setting.usage_ping_can_be_configured?).to be_falsey
+ end
+
+ context 'when the usage ping is disabled in the DB' do
+ before do
+ setting.usage_ping_enabled = false
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+
+ context 'when the usage ping is enabled in the DB' do
+ before do
+ setting.usage_ping_enabled = true
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+ end
+
+ context 'when the usage ping is enabled in gitlab.yml' do
+ before do
+ allow(Settings.gitlab).to receive(:usage_ping_enabled).and_return(true)
+ end
+
+ it 'allows the usage ping to be configured' do
+ expect(setting.usage_ping_can_be_configured?).to be_truthy
+ end
+
+ context 'when the usage ping is disabled in the DB' do
+ before do
+ setting.usage_ping_enabled = false
+ end
+
+ it 'returns false for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_falsey
+ end
+ end
+
+ context 'when the usage ping is enabled in the DB' do
+ before do
+ setting.usage_ping_enabled = true
+ end
+
+ it 'returns true for usage_ping_enabled' do
+ expect(setting.usage_ping_enabled).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe '#allowed_key_types' do
+ it 'includes all key types by default' do
+ expect(setting.allowed_key_types).to contain_exactly(*described_class::SUPPORTED_KEY_TYPES)
+ end
+
+ it 'excludes disabled key types' do
+ expect(setting.allowed_key_types).to include(:ed25519)
+
+ setting.ed25519_key_restriction = described_class::FORBIDDEN_KEY_VALUE
+
+ expect(setting.allowed_key_types).not_to include(:ed25519)
+ end
+ end
+
+ describe '#key_restriction_for' do
+ it 'returns the restriction value for recognised types' do
+ setting.rsa_key_restriction = 1024
+
+ expect(setting.key_restriction_for(:rsa)).to eq(1024)
+ end
+
+ it 'allows types to be passed as a string' do
+ setting.rsa_key_restriction = 1024
+
+ expect(setting.key_restriction_for('rsa')).to eq(1024)
+ end
+
+ it 'returns forbidden for unrecognised type' do
+ expect(setting.key_restriction_for(:foo)).to eq(described_class::FORBIDDEN_KEY_VALUE)
+ end
+ end
+
+ describe '#allow_signup?' do
+ it 'returns true' do
+ expect(setting.allow_signup?).to be_truthy
+ end
+
+ it 'returns false if signup is disabled' do
+ allow(setting).to receive(:signup_enabled?).and_return(false)
+
+ expect(setting.allow_signup?).to be_falsey
+ end
+
+ it 'returns false if password authentication is disabled for the web interface' do
+ allow(setting).to receive(:password_authentication_enabled_for_web?).and_return(false)
+
+ expect(setting.allow_signup?).to be_falsey
+ end
+ end
+
+ describe '#pick_repository_storage' do
+ it 'uses Array#sample to pick a random storage' do
+ array = double('array', sample: 'random')
+ expect(setting).to receive(:repository_storages).and_return(array)
+
+ expect(setting.pick_repository_storage).to eq('random')
+ end
+ end
+
+ describe '#user_default_internal_regex_enabled?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:user_default_external, :user_default_internal_regex, :result) do
+ false | nil | false
+ false | '' | false
+ false | '^(?:(?!\.ext@).)*$\r?\n?' | false
+ true | '' | false
+ true | nil | false
+ true | '^(?:(?!\.ext@).)*$\r?\n?' | true
+ end
+
+ with_them do
+ before do
+ setting.user_default_external = user_default_external
+ setting.user_default_internal_regex = user_default_internal_regex
+ end
+
+ subject { setting.user_default_internal_regex_enabled? }
+
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ describe '#archive_builds_older_than' do
+ subject { setting.archive_builds_older_than }
+
+ context 'when the archive_builds_in_seconds is set' do
+ before do
+ setting.archive_builds_in_seconds = 3600
+ end
+
+ it { is_expected.to be_within(1.minute).of(1.hour.ago) }
+ end
+
+ context 'when the archive_builds_in_seconds is set' do
+ before do
+ setting.archive_builds_in_seconds = nil
+ end
+
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#commit_email_hostname' do
+ context 'when the value is provided' do
+ before do
+ setting.commit_email_hostname = 'localhost'
+ end
+
+ it 'returns the provided value' do
+ expect(setting.commit_email_hostname).to eq('localhost')
+ end
+ end
+
+ context 'when the value is not provided' do
+ it 'returns the default from the class' do
+ expect(setting.commit_email_hostname)
+ .to eq(described_class.default_commit_email_hostname)
+ end
+ end
+ end
+
+ it 'predicate method changes when value is updated' do
+ setting.password_authentication_enabled_for_web = false
+
+ expect(setting.password_authentication_enabled_for_web?).to be_falsey
+ end
+
+ describe 'sentry settings' do
+ context 'when the sentry settings are not set in gitlab.yml' do
+ it 'fallbacks to the settings in the database' do
+ setting.sentry_enabled = true
+ setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ setting.clientside_sentry_enabled = true
+ setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(false)
+ allow(Gitlab.config.sentry).to receive(:dsn).and_return(nil)
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return(nil)
+
+ expect(setting.sentry_enabled).to eq true
+ expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ expect(setting.clientside_sentry_enabled).to eq true
+ expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+ end
+ end
+
+ context 'when the sentry settings are set in gitlab.yml' do
+ it 'does not fallback to the settings in the database' do
+ setting.sentry_enabled = false
+ setting.sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/40'
+ setting.clientside_sentry_enabled = false
+ setting.clientside_sentry_dsn = 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/41'
+
+ allow(Gitlab.config.sentry).to receive(:enabled).and_return(true)
+ allow(Gitlab.config.sentry).to receive(:dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42')
+ allow(Gitlab.config.sentry).to receive(:clientside_dsn).and_return('https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43')
+
+ expect(setting).not_to receive(:read_attribute)
+ expect(setting.sentry_enabled).to eq true
+ expect(setting.sentry_dsn).to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/42'
+ expect(setting.clientside_sentry_enabled).to eq true
+ expect(setting.clientside_sentry_dsn). to eq 'https://b44a0828b72421a6d8e99efd68d44fa8@example.com/43'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/ci/stage_shared_examples.rb b/spec/support/shared_examples/ci/stage_shared_examples.rb
new file mode 100644
index 00000000000..925974ed11e
--- /dev/null
+++ b/spec/support/shared_examples/ci/stage_shared_examples.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+shared_examples 'manual playable stage' do |stage_type|
+ let(:stage) { build(stage_type, status: status) }
+
+ describe '#manual_playable?' do
+ subject { stage.manual_playable? }
+
+ context 'when is manual' do
+ let(:status) { 'manual' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when is scheduled' do
+ let(:status) { 'scheduled' }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when is skipped' do
+ let(:status) { 'skipped' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/ci_trace_shared_examples.rb b/spec/support/shared_examples/ci_trace_shared_examples.rb
index c603421d748..db935bcb388 100644
--- a/spec/support/shared_examples/ci_trace_shared_examples.rb
+++ b/spec/support/shared_examples/ci_trace_shared_examples.rb
@@ -329,14 +329,6 @@ shared_examples_for 'trace with disabled live trace feature' do
it_behaves_like 'read successfully with IO'
end
- context 'when current_path (with project_ci_id) exists' do
- before do
- expect(trace).to receive(:deprecated_path) { expand_fixture_path('trace/sample_trace') }
- end
-
- it_behaves_like 'read successfully with IO'
- end
-
context 'when db trace exists' do
before do
build.send(:write_attribute, :trace, "data")
@@ -396,37 +388,6 @@ shared_examples_for 'trace with disabled live trace feature' do
end
end
- context 'deprecated path' do
- let(:path) { trace.send(:deprecated_path) }
-
- context 'with valid ci_id' do
- before do
- build.project.update(ci_id: 1000)
-
- FileUtils.mkdir_p(File.dirname(path))
-
- File.open(path, "w") do |file|
- file.write("data")
- end
- end
-
- it "trace exist" do
- expect(trace.exist?).to be(true)
- end
-
- it "can be erased" do
- trace.erase!
- expect(trace.exist?).to be(false)
- end
- end
-
- context 'without valid ci_id' do
- it "does not return deprecated path" do
- expect(path).to be_nil
- end
- end
- end
-
context 'stored in database' do
before do
build.send(:write_attribute, :trace, "data")
diff --git a/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb
new file mode 100644
index 00000000000..8dd78fd0a25
--- /dev/null
+++ b/spec/support/shared_examples/controllers/external_authorization_service_shared_examples.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+shared_examples 'disabled when using an external authorization service' do
+ include ExternalAuthorizationServiceHelpers
+
+ it 'works when the feature is not enabled' do
+ subject
+
+ expect(response).to be_success
+ end
+
+ it 'renders a 404 with a message when the feature is enabled' do
+ enable_external_authorization_service_check
+
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+end
+
+shared_examples 'unauthorized when external service denies access' do
+ include ExternalAuthorizationServiceHelpers
+
+ it 'allows access when the authorization service allows it' do
+ external_service_allow_access(user, project)
+
+ subject
+
+ # Account for redirects after updates
+ expect(response.status).to be_between(200, 302)
+ end
+
+ it 'allows access when the authorization service denies it' do
+ external_service_deny_access(user, project)
+
+ subject
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+end
diff --git a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
index 982e0317f7f..b7080c68270 100644
--- a/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
+++ b/spec/support/shared_examples/controllers/repository_lfs_file_load_examples.rb
@@ -9,87 +9,87 @@
# - `filepath`: path of the file (contains filename)
# - `subject`: the request to be made to the controller. Example:
# subject { get :show, namespace_id: project.namespace, project_id: project }
-shared_examples 'repository lfs file load' do
- context 'when file is stored in lfs' do
- let(:lfs_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
- let(:lfs_size) { '1575078' }
- let!(:lfs_object) { create(:lfs_object, oid: lfs_oid, size: lfs_size) }
+shared_examples 'a controller that can serve LFS files' do
+ let(:lfs_oid) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
+ let(:lfs_size) { '1575078' }
+ let!(:lfs_object) { create(:lfs_object, oid: lfs_oid, size: lfs_size) }
+
+ context 'when lfs is enabled' do
+ before do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
+ end
- context 'when lfs is enabled' do
+ context 'when project has access' do
before do
- allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true)
+ project.lfs_objects << lfs_object
+ allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
+ allow(controller).to receive(:send_file) { controller.head :ok }
end
- context 'when project has access' do
- before do
- project.lfs_objects << lfs_object
- allow_any_instance_of(LfsObjectUploader).to receive(:exists?).and_return(true)
- allow(controller).to receive(:send_file) { controller.head :ok }
- end
+ it 'serves the file' do
+ lfs_uploader = LfsObjectUploader.new(lfs_object)
- it 'serves the file' do
- # Notice the filename= is omitted from the disposition; this is because
- # Rails 5 will append this header in send_file
- expect(controller).to receive(:send_file)
- .with(
- "#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897",
- filename: filename,
- disposition: %Q(attachment; filename*=UTF-8''#{filename}))
+ # Notice the filename= is omitted from the disposition; this is because
+ # Rails 5 will append this header in send_file
+ expect(controller).to receive(:send_file)
+ .with(
+ File.join(lfs_uploader.root, lfs_uploader.store_dir, lfs_uploader.filename),
+ filename: filename,
+ disposition: %Q(attachment; filename*=UTF-8''#{filename}))
- subject
+ subject
- expect(response).to have_gitlab_http_status(200)
- end
+ expect(response).to have_gitlab_http_status(200)
+ end
- context 'and lfs uses object storage' do
- let(:lfs_object) { create(:lfs_object, :with_file, oid: lfs_oid, size: lfs_size) }
+ context 'and lfs uses object storage' do
+ let(:lfs_object) { create(:lfs_object, :with_file, oid: lfs_oid, size: lfs_size) }
- before do
- stub_lfs_object_storage
- lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
- end
+ before do
+ stub_lfs_object_storage
+ lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
+ end
- it 'responds with redirect to file' do
- subject
+ it 'responds with redirect to file' do
+ subject
- expect(response).to have_gitlab_http_status(302)
- expect(response.location).to include(lfs_object.reload.file.path)
- end
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.location).to include(lfs_object.reload.file.path)
+ end
- it 'sets content disposition' do
- subject
+ it 'sets content disposition' do
+ subject
- file_uri = URI.parse(response.location)
- params = CGI.parse(file_uri.query)
+ file_uri = URI.parse(response.location)
+ params = CGI.parse(file_uri.query)
- expect(params["response-content-disposition"].first).to eq(%q(attachment; filename="lfs_object.iso"; filename*=UTF-8''lfs_object.iso))
- end
+ expect(params["response-content-disposition"].first).to eq(%Q(attachment; filename="#{filename}"; filename*=UTF-8''#{filename}))
end
end
+ end
- context 'when project does not have access' do
- it 'does not serve the file' do
- subject
+ context 'when project does not have access' do
+ it 'does not serve the file' do
+ subject
- expect(response).to have_gitlab_http_status(404)
- end
+ expect(response).to have_gitlab_http_status(404)
end
end
+ end
- context 'when lfs is not enabled' do
- before do
- allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
- end
+ context 'when lfs is not enabled' do
+ before do
+ allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(false)
+ end
- it 'delivers ASCII file' do
- subject
+ it 'delivers ASCII file' do
+ subject
- expect(response).to have_gitlab_http_status(200)
- expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(response.header['Content-Disposition'])
- .to eq('inline')
- expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
- end
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.header['Content-Disposition'])
+ .to eq('inline')
+ expect(response.header[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with('git-blob:')
end
end
end
diff --git a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
index 98ab04c5636..eb051166a69 100644
--- a/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/set_sort_order_from_user_preference_shared_examples.rb
@@ -4,7 +4,7 @@ shared_examples 'set sort order from user preference' do
# however any other field present in user_preferences table can be used for testing.
context 'when database is in read-only mode' do
- it 'it does not update user preference' do
+ it 'does not update user preference' do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
expect_any_instance_of(UserPreference).not_to receive(:update).with({ controller.send(:issuable_sorting_field) => sorting_param })
diff --git a/spec/support/shared_examples/controllers/variables_shared_examples.rb b/spec/support/shared_examples/controllers/variables_shared_examples.rb
index b615a8f54cf..e80722857ec 100644
--- a/spec/support/shared_examples/controllers/variables_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/variables_shared_examples.rb
@@ -120,4 +120,16 @@ shared_examples 'PATCH #update updates variables' do
expect(response).to match_response_schema('variables')
end
end
+
+ context 'for variables of type file' do
+ let(:variables_attributes) do
+ [
+ new_variable_attributes.merge(variable_type: 'file')
+ ]
+ end
+
+ it 'creates new variable of type file' do
+ expect { subject }.to change { owner.variables.file.count }.by(1)
+ end
+ end
end
diff --git a/spec/support/shared_examples/dirty_submit_form_shared_examples.rb b/spec/support/shared_examples/dirty_submit_form_shared_examples.rb
index 52a2ee49495..4e45e2921e7 100644
--- a/spec/support/shared_examples/dirty_submit_form_shared_examples.rb
+++ b/spec/support/shared_examples/dirty_submit_form_shared_examples.rb
@@ -1,18 +1,17 @@
shared_examples 'dirty submit form' do |selector_args|
selectors = selector_args.is_a?(Array) ? selector_args : [selector_args]
- def expect_disabled_state(form, submit, is_disabled = true)
+ def expect_disabled_state(form, submit_selector, is_disabled = true)
disabled_selector = is_disabled == true ? '[disabled]' : ':not([disabled])'
- form.find(".js-dirty-submit#{disabled_selector}", match: :first)
-
- expect(submit.disabled?).to be is_disabled
+ form.find("#{submit_selector}#{disabled_selector}")
end
selectors.each do |selector|
it "disables #{selector[:form]} submit until there are changes on #{selector[:input]}", :js do
form = find(selector[:form])
- submit = form.first('.js-dirty-submit')
+ submit_selector = selector[:submit] || 'input[type="submit"]'
+ submit = form.first(submit_selector)
input = form.first(selector[:input])
is_radio = input[:type] == 'radio'
is_checkbox = input[:type] == 'checkbox'
@@ -22,15 +21,14 @@ shared_examples 'dirty submit form' do |selector_args|
original_checkable = input if is_checkbox
expect(submit.disabled?).to be true
- expect(input.checked?).to be false
is_checkable ? input.click : input.set("#{original_value} changes")
- expect_disabled_state(form, submit, false)
+ expect_disabled_state(form, submit_selector, false)
is_checkable ? original_checkable.click : input.set(original_value)
- expect_disabled_state(form, submit)
+ expect_disabled_state(form, submit_selector)
end
end
end
diff --git a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
index 7038a366144..ec1b1754cf0 100644
--- a/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/creatable_merge_request_shared_examples.rb
@@ -1,42 +1,17 @@
RSpec.shared_examples 'a creatable merge request' do
include WaitForRequests
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:target_project) { create(:project, :public, :repository) }
- let(:source_project) { target_project }
- let!(:milestone) { create(:milestone, project: target_project) }
- let!(:label) { create(:label, project: target_project) }
- let!(:label2) { create(:label, project: target_project) }
-
- before do
- source_project.add_maintainer(user)
- target_project.add_maintainer(user)
- target_project.add_maintainer(user2)
-
- sign_in(user)
- visit project_new_merge_request_path(
- target_project,
- merge_request: {
- source_project_id: source_project.id,
- target_project_id: target_project.id,
- source_branch: 'fix',
- target_branch: 'master'
- })
- end
-
it 'creates new merge request', :js do
- click_button 'Assignee'
+ find('.js-assignee-search').click
page.within '.dropdown-menu-user' do
click_link user2.name
end
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user2.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user2.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user2.name
end
-
click_link 'Assign to me'
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
index eef0327c9a6..a6121fcc50a 100644
--- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
+++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb
@@ -1,34 +1,10 @@
RSpec.shared_examples 'an editable merge request' do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let!(:milestone) { create(:milestone, project: target_project) }
- let!(:label) { create(:label, project: target_project) }
- let!(:label2) { create(:label, project: target_project) }
- let(:target_project) { create(:project, :public, :repository) }
- let(:source_project) { target_project }
- let(:merge_request) do
- create(:merge_request,
- source_project: source_project,
- target_project: target_project,
- source_branch: 'fix',
- target_branch: 'master')
- end
-
- before do
- source_project.add_maintainer(user)
- target_project.add_maintainer(user)
- target_project.add_maintainer(user2)
-
- sign_in(user)
- visit edit_project_merge_request_path(target_project, merge_request)
- end
-
it 'updates merge request', :js do
- click_button 'Assignee'
+ find('.js-assignee-search').click
page.within '.dropdown-menu-user' do
click_link user.name
end
- expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="merge_request[assignee_ids][]"]', visible: false).value).to match(user.id.to_s)
page.within '.js-assignee-search' do
expect(page).to have_content user.name
end
diff --git a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
index 75ad948e42c..d87e5fcaa88 100644
--- a/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
+++ b/spec/support/shared_examples/features/master_manages_access_requests_shared_example.rb
@@ -19,7 +19,7 @@ RSpec.shared_examples 'Maintainer manages access requests' do
expect_visible_access_request(entity, user)
- accept_confirm { click_on 'Grant access' }
+ click_on 'Grant access'
expect_no_visible_access_request(entity, user)
@@ -40,13 +40,11 @@ RSpec.shared_examples 'Maintainer manages access requests' do
end
def expect_visible_access_request(entity, user)
- expect(entity.requesters.exists?(user_id: user)).to be_truthy
expect(page).to have_content "Users requesting access to #{entity.name} 1"
expect(page).to have_content user.name
end
def expect_no_visible_access_request(entity, user)
- expect(entity.requesters.exists?(user_id: user)).to be_falsy
expect(page).not_to have_content "Users requesting access to #{entity.name}"
end
end
diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
new file mode 100644
index 00000000000..bab7963f06f
--- /dev/null
+++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+shared_examples 'multiple assignees merge request' do |action, save_button_title|
+ it "#{action} a MR with multiple assignees", :js do
+ find('.js-assignee-search').click
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ click_link user2.name
+ end
+
+ # Extra click needed in order to toggle the dropdown
+ find('.js-assignee-search').click
+
+ expect(all('input[name="merge_request[assignee_ids][]"]', visible: false).map(&:value))
+ .to match_array([user.id.to_s, user2.id.to_s])
+
+ page.within '.js-assignee-search' do
+ expect(page).to have_content "#{user2.name} + 1 more"
+ end
+
+ click_button save_button_title
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content '2 Assignees'
+
+ click_link 'Edit'
+
+ expect(page).to have_content user.name
+ expect(page).to have_content user2.name
+ end
+ end
+
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ # Closing dropdown to persist
+ click_link 'Edit'
+
+ expect(page).to have_content user2.name
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb b/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb
new file mode 100644
index 00000000000..782a2d97746
--- /dev/null
+++ b/spec/support/shared_examples/finders/assignees_filter_shared_examples.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+shared_examples 'assignee ID filter' do
+ it 'returns issuables assigned to that user' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'assignee username filter' do
+ it 'returns issuables assigned to those users' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'no assignee filter' do
+ let(:params) { { assignee_id: 'None' } }
+
+ it 'returns issuables not assigned to any assignee' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables not assigned to any assignee' do
+ params[:assignee_id] = 0
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables not assigned to any assignee' do
+ params[:assignee_id] = 'none'
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+end
+
+shared_examples 'any assignee filter' do
+ context '' do
+ let(:params) { { assignee_id: 'Any' } }
+
+ it 'returns issuables assigned to any assignee' do
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+
+ it 'returns issuables assigned to any assignee' do
+ params[:assignee_id] = 'any'
+
+ expect(issuables).to contain_exactly(*expected_issuables)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb b/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb
new file mode 100644
index 00000000000..d7e17cc0b70
--- /dev/null
+++ b/spec/support/shared_examples/finders/finder_with_external_authorization_enabled.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+shared_examples 'a finder with external authorization service' do
+ include ExternalAuthorizationServiceHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'finds the subject' do
+ expect(described_class.new(user).execute).to include(subject)
+ end
+
+ context 'with an external authorization service' do
+ before do
+ enable_external_authorization_service_check
+ end
+
+ it 'does not include the subject when no project was given' do
+ expect(described_class.new(user).execute).not_to include(subject)
+ end
+
+ it 'includes the subject when a project id was given' do
+ expect(described_class.new(user, project_params).execute).to include(subject)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/helm_generated_script.rb b/spec/support/shared_examples/helm_generated_script.rb
index ba9b7d3bdcf..01bee603274 100644
--- a/spec/support/shared_examples/helm_generated_script.rb
+++ b/spec/support/shared_examples/helm_generated_script.rb
@@ -6,7 +6,7 @@ shared_examples 'helm commands' do
EOS
end
- it 'should return appropriate command' do
+ it 'returns appropriate command' do
expect(subject.generate_script.strip).to eq((helm_setup + commands).strip)
end
end
diff --git a/spec/support/shared_examples/issuable_shared_examples.rb b/spec/support/shared_examples/issuable_shared_examples.rb
index c3d40c5b231..d97b21f71cd 100644
--- a/spec/support/shared_examples/issuable_shared_examples.rb
+++ b/spec/support/shared_examples/issuable_shared_examples.rb
@@ -31,7 +31,7 @@ shared_examples 'system notes for milestones' do
context 'project milestones' do
it 'creates a system note' do
expect do
- update_issuable(milestone: create(:milestone))
+ update_issuable(milestone: create(:milestone, project: project))
end.to change { Note.system.count }.by(1)
end
end
diff --git a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
index 90d67fd00fc..244f4766a84 100644
--- a/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
+++ b/spec/support/shared_examples/issuables_list_metadata_shared_examples.rb
@@ -1,11 +1,11 @@
shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
include ProjectForksHelper
- def get_action(action, project)
+ def get_action(action, project, extra_params = {})
if action
- get action, params: { author_id: project.creator.id }
+ get action, params: { author_id: project.creator.id }.merge(extra_params)
else
- get :index, params: { namespace_id: project.namespace, project_id: project }
+ get :index, params: { namespace_id: project.namespace, project_id: project }.merge(extra_params)
end
end
@@ -17,23 +17,44 @@ shared_examples 'issuables list meta-data' do |issuable_type, action = nil|
end
end
- before do
- @issuable_ids = %w[fix improve/awesome].map do |source_branch|
- create_issuable(issuable_type, project, source_branch: source_branch).id
+ let!(:issuables) do
+ %w[fix improve/awesome].map do |source_branch|
+ create_issuable(issuable_type, project, source_branch: source_branch)
end
end
+ let(:issuable_ids) { issuables.map(&:id) }
+
it "creates indexed meta-data object for issuable notes and votes count" do
get_action(action, project)
meta_data = assigns(:issuable_meta_data)
aggregate_failures do
- expect(meta_data.keys).to match_array(@issuable_ids)
+ expect(meta_data.keys).to match_array(issuables.map(&:id))
expect(meta_data.values).to all(be_kind_of(Issuable::IssuableMeta))
end
end
+ context 'searching' do
+ let(:result_issuable) { issuables.first }
+ let(:search) { result_issuable.title }
+
+ before do
+ stub_feature_flags(attempt_project_search_optimizations: true)
+ end
+
+ # .simple_sorts is the same across all Sortable classes
+ sorts = ::Issue.simple_sorts.keys + %w[popularity priority label_priority]
+ sorts.each do |sort|
+ it "works when sorting by #{sort}" do
+ get_action(action, project, search: search, sort: sort)
+
+ expect(assigns(:issuable_meta_data).keys).to include(result_issuable.id)
+ end
+ end
+ end
+
it "avoids N+1 queries" do
control = ActiveRecord::QueryRecorder.new { get_action(action, project) }
issuable = create_issuable(issuable_type, project, source_branch: 'csv')
diff --git a/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb b/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb
index f300bdd48b1..f326e502092 100644
--- a/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb
+++ b/spec/support/shared_examples/legacy_path_redirect_shared_examples.rb
@@ -11,3 +11,11 @@ shared_examples 'redirecting a legacy path' do |source, target|
expect(get(source)).not_to redirect_to(target)
end
end
+
+shared_examples 'redirecting a legacy project path' do |source, target|
+ include RSpec::Rails::RequestExampleGroup
+
+ it "redirects #{source} to #{target}" do
+ expect(get(source)).to redirect_to(target)
+ end
+end
diff --git a/spec/support/shared_examples/malicious_regexp_shared_examples.rb b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
index db69b75c0c8..a86050e2cf2 100644
--- a/spec/support/shared_examples/malicious_regexp_shared_examples.rb
+++ b/spec/support/shared_examples/malicious_regexp_shared_examples.rb
@@ -2,7 +2,8 @@ require 'timeout'
shared_examples 'malicious regexp' do
let(:malicious_text) { 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!' }
- let(:malicious_regexp) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' }
+ let(:malicious_regexp_re2) { '(?i)^(([a-z])+.)+[A-Z]([a-z])+$' }
+ let(:malicious_regexp_ruby) { '/^(([a-z])+.)+[A-Z]([a-z])+$/i' }
it 'takes under a second' do
expect { Timeout.timeout(1) { subject } }.not_to raise_error
diff --git a/spec/support/shared_examples/models/atomic_internal_id_spec.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
index c659be8f13a..a248f60d23e 100644
--- a/spec/support/shared_examples/models/atomic_internal_id_spec.rb
+++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb
@@ -52,20 +52,20 @@ shared_examples_for 'AtomicInternalId' do |validate_presence: true|
expect(InternalId).to receive(:generate_next).with(instance, scope_attrs, usage, any_args).and_return(iid)
subject
- expect(instance.public_send(internal_id_attribute)).to eq(iid)
+ expect(read_internal_id).to eq(iid)
end
it 'does not overwrite an existing internal id' do
- instance.public_send("#{internal_id_attribute}=", 4711)
+ write_internal_id(4711)
- expect { subject }.not_to change { instance.public_send(internal_id_attribute) }
+ expect { subject }.not_to change { read_internal_id }
end
context 'when the instance has an internal ID set' do
let(:internal_id) { 9001 }
it 'calls InternalId.update_last_value and sets the `last_value` to that of the instance' do
- instance.send("#{internal_id_attribute}=", internal_id)
+ write_internal_id(internal_id)
expect(InternalId)
.to receive(:track_greatest)
@@ -75,5 +75,39 @@ shared_examples_for 'AtomicInternalId' do |validate_presence: true|
end
end
end
+
+ describe "#reset_scope_internal_id_attribute" do
+ it 'rewinds the allocated IID' do
+ expect { ensure_scope_attribute! }.not_to raise_error
+ expect(read_internal_id).not_to be_nil
+
+ expect(reset_scope_attribute).to be_nil
+ expect(read_internal_id).to be_nil
+ end
+
+ it 'allocates the same IID' do
+ internal_id = ensure_scope_attribute!
+ reset_scope_attribute
+ expect(read_internal_id).to be_nil
+
+ expect(ensure_scope_attribute!).to eq(internal_id)
+ end
+ end
+
+ def ensure_scope_attribute!
+ instance.public_send(:"ensure_#{scope}_#{internal_id_attribute}!")
+ end
+
+ def reset_scope_attribute
+ instance.public_send(:"reset_#{scope}_#{internal_id_attribute}")
+ end
+
+ def read_internal_id
+ instance.public_send(internal_id_attribute)
+ end
+
+ def write_internal_id(value)
+ instance.public_send(:"#{internal_id_attribute}=", value)
+ end
end
end
diff --git a/spec/support/shared_examples/models/chat_service_spec.rb b/spec/support/shared_examples/models/chat_service_shared_examples.rb
index cf1d52a9616..0a302e7d030 100644
--- a/spec/support/shared_examples/models/chat_service_spec.rb
+++ b/spec/support/shared_examples/models/chat_service_shared_examples.rb
@@ -25,6 +25,12 @@ shared_examples_for "chat service" do |service_name|
end
end
+ describe '.supported_events' do
+ it 'does not support deployment_events' do
+ expect(described_class.supported_events).not_to include('deployment')
+ end
+ end
+
describe "#execute" do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
@@ -64,7 +70,7 @@ shared_examples_for "chat service" do |service_name|
context "with not default branch" do
let(:sample_data) do
- Gitlab::DataBuilder::Push.build(project, user, nil, nil, "not-the-default-branch")
+ Gitlab::DataBuilder::Push.build(project: project, user: user, ref: "not-the-default-branch")
end
context "when notify_only_default_branch enabled" do
diff --git a/spec/support/shared_examples/models/ci_variable_shared_examples.rb b/spec/support/shared_examples/models/ci_variable_shared_examples.rb
new file mode 100644
index 00000000000..f93de8b6ff1
--- /dev/null
+++ b/spec/support/shared_examples/models/ci_variable_shared_examples.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+shared_examples_for 'CI variable' do
+ it { is_expected.to include_module(HasVariable) }
+
+ describe "variable type" do
+ it 'defines variable types' do
+ expect(described_class.variable_types).to eq({ "env_var" => 1, "file" => 2 })
+ end
+
+ it "defaults variable type to env_var" do
+ expect(subject.variable_type).to eq("env_var")
+ end
+
+ it "supports variable type file" do
+ variable = described_class.new(variable_type: :file)
+ expect(variable).to be_file
+ end
+ end
+
+ it 'strips whitespaces when assigning key' do
+ subject.key = " SECRET "
+ expect(subject.key).to eq("SECRET")
+ end
+
+ it 'can convert to runner variable' do
+ expect(subject.to_runner_variable.keys).to include(:key, :value, :public, :file)
+ end
+end
diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
index 1f76b981292..d6490a808ce 100644
--- a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
@@ -2,6 +2,14 @@ shared_examples 'cluster application core specs' do |application_name|
it { is_expected.to belong_to(:cluster) }
it { is_expected.to validate_presence_of(:cluster) }
+ describe '#can_uninstall?' do
+ it 'calls allowed_to_uninstall?' do
+ expect(subject).to receive(:allowed_to_uninstall?).and_return(true)
+
+ expect(subject.can_uninstall?).to be_truthy
+ end
+ end
+
describe '#name' do
it 'is .application_name' do
expect(subject.name).to eq(described_class.application_name)
diff --git a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
index d87b3181e80..bd3661471f8 100644
--- a/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_helm_cert_examples.rb
@@ -1,6 +1,32 @@
shared_examples 'cluster application helm specs' do |application_name|
let(:application) { create(application_name) }
+ describe '#uninstall_command' do
+ subject { application.uninstall_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::DeleteCommand) }
+
+ it 'has the application name' do
+ expect(subject.name).to eq(application.name)
+ end
+
+ it 'has files' do
+ expect(subject.files).to eq(application.files)
+ end
+
+ it 'is rbac' do
+ expect(subject).to be_rbac
+ end
+
+ context 'on a non rbac enabled cluster' do
+ before do
+ application.cluster.platform_kubernetes.abac!
+ end
+
+ it { is_expected.not_to be_rbac }
+ end
+ end
+
describe '#files' do
subject { application.files }
@@ -9,12 +35,12 @@ shared_examples 'cluster application helm specs' do |application_name|
application.cluster.application_helm.ca_cert = nil
end
- it 'should not include cert files when there is no ca_cert entry' do
+ it 'does not include cert files when there is no ca_cert entry' do
expect(subject).not_to include(:'ca.pem', :'cert.pem', :'key.pem')
end
end
- it 'should include cert files when there is a ca_cert entry' do
+ it 'includes cert files when there is a ca_cert entry' do
expect(subject).to include(:'ca.pem', :'cert.pem', :'key.pem')
expect(subject[:'ca.pem']).to eq(application.cluster.application_helm.ca_cert)
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
index b8c19cab0c4..4525c03837f 100644
--- a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -114,6 +114,17 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to eq(reason)
end
end
+
+ context 'application is uninstalling' do
+ subject { create(application_name, :uninstalling) }
+
+ it 'is uninstall_errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_uninstall_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
end
describe '#make_scheduled' do
@@ -125,6 +136,16 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject).to be_scheduled
end
+ describe 'when installed' do
+ subject { create(application_name, :installed) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+ end
+
describe 'when was errored' do
subject { create(application_name, :errored) }
@@ -148,6 +169,28 @@ shared_examples 'cluster application status specs' do |application_name|
expect(subject.status_reason).to be_nil
end
end
+
+ describe 'when was uninstall_errored' do
+ subject { create(application_name, :uninstall_errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+
+ describe '#make_uninstalling' do
+ subject { create(application_name, :scheduled) }
+
+ it 'is uninstalling' do
+ subject.make_uninstalling!
+
+ expect(subject).to be_uninstalling
+ end
end
end
@@ -155,16 +198,18 @@ shared_examples 'cluster application status specs' do |application_name|
using RSpec::Parameterized::TableSyntax
where(:trait, :available) do
- :not_installable | false
- :installable | false
- :scheduled | false
- :installing | false
- :installed | true
- :updating | false
- :updated | true
- :errored | false
- :update_errored | false
- :timeouted | false
+ :not_installable | false
+ :installable | false
+ :scheduled | false
+ :installing | false
+ :installed | true
+ :updating | false
+ :updated | true
+ :errored | false
+ :update_errored | false
+ :uninstalling | false
+ :uninstall_errored | false
+ :timed_out | false
end
with_them do
diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb
index 77376496854..e5375bc8280 100644
--- a/spec/support/shared_examples/models/member_shared_examples.rb
+++ b/spec/support/shared_examples/models/member_shared_examples.rb
@@ -41,7 +41,7 @@ shared_examples_for 'inherited access level as a member of entity' do
member.update(access_level: Gitlab::Access::REPORTER)
- expect(member.errors.full_messages).to eq(["Access level should be higher than Developer inherited membership from group #{parent_entity.name}"])
+ expect(member.errors.full_messages).to eq(["Access level should be greater than or equal to Developer inherited membership from group #{parent_entity.name}"])
end
it 'allows changing the level from a non existing member' do
diff --git a/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
new file mode 100644
index 00000000000..1b09c3dd636
--- /dev/null
+++ b/spec/support/shared_examples/models/update_project_statistics_shared_examples.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+shared_examples_for 'UpdateProjectStatistics' do
+ let(:project) { subject.project }
+ let(:project_statistics_name) { described_class.project_statistics_name }
+ let(:statistic_attribute) { described_class.statistic_attribute }
+
+ def reload_stat
+ project.statistics.reload.send(project_statistics_name).to_i
+ end
+
+ def read_attribute
+ subject.read_attribute(statistic_attribute).to_i
+ end
+
+ it { is_expected.to be_new_record }
+
+ context 'when creating' do
+ it 'updates the project statistics' do
+ delta = read_attribute
+
+ expect { subject.save! }
+ .to change { reload_stat }
+ .by(delta)
+ end
+ end
+
+ context 'when updating' do
+ before do
+ subject.save!
+ end
+
+ it 'updates project statistics' do
+ delta = 42
+
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ subject.write_attribute(statistic_attribute, read_attribute + delta)
+
+ expect { subject.save! }
+ .to change { reload_stat }
+ .by(delta)
+ end
+ end
+
+ context 'when destroying' do
+ before do
+ subject.save!
+ end
+
+ it 'updates the project statistics' do
+ delta = -read_attribute
+
+ expect(ProjectStatistics)
+ .to receive(:increment_statistic)
+ .and_call_original
+
+ expect { subject.destroy }
+ .to change { reload_stat }
+ .by(delta)
+ end
+
+ context 'when it is destroyed from the project level' do
+ it 'does not update the project statistics' do
+ expect(ProjectStatistics)
+ .not_to receive(:increment_statistic)
+
+ project.update(pending_delete: true)
+ project.destroy!
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/notify_shared_examples.rb b/spec/support/shared_examples/notify_shared_examples.rb
index a38354060cf..897c9106d77 100644
--- a/spec/support/shared_examples/notify_shared_examples.rb
+++ b/spec/support/shared_examples/notify_shared_examples.rb
@@ -1,5 +1,7 @@
shared_context 'gitlab email notification' do
- set(:project) { create(:project, :repository, name: 'a-known-name') }
+ set(:group) { create(:group) }
+ set(:subgroup) { create(:group, parent: group) }
+ set(:project) { create(:project, :repository, name: 'a-known-name', group: group) }
set(:recipient) { create(:user, email: 'recipient@example.com') }
let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
@@ -39,6 +41,47 @@ shared_examples 'an email sent from GitLab' do
end
end
+shared_examples 'an email sent to a user' do
+ let(:group_notification_email) { 'user+group@example.com' }
+
+ it 'is sent to user\'s global notification email address' do
+ expect(subject).to deliver_to(test_recipient.notification_email)
+ end
+
+ context 'that is part of a project\'s group' do
+ it 'is sent to user\'s group notification email address when set' do
+ create(:notification_setting, user: test_recipient, source: project.group, notification_email: group_notification_email)
+ expect(subject).to deliver_to(group_notification_email)
+ end
+
+ it 'is sent to user\'s global notification email address when no group email set' do
+ create(:notification_setting, user: test_recipient, source: project.group, notification_email: '')
+ expect(subject).to deliver_to(test_recipient.notification_email)
+ end
+ end
+
+ context 'when project is in a sub-group', :nested_groups do
+ before do
+ project.update!(group: subgroup)
+ end
+
+ it 'is sent to user\'s subgroup notification email address when set' do
+ # Set top-level group notification email address to make sure it doesn't get selected
+ create(:notification_setting, user: test_recipient, source: group, notification_email: group_notification_email)
+
+ subgroup_notification_email = 'user+subgroup@example.com'
+ create(:notification_setting, user: test_recipient, source: subgroup, notification_email: subgroup_notification_email)
+
+ expect(subject).to deliver_to(subgroup_notification_email)
+ end
+
+ it 'is sent to user\'s group notification email address when set and subgroup email address not set' do
+ create(:notification_setting, user: test_recipient, source: subgroup, notification_email: '')
+ expect(subject).to deliver_to(test_recipient.notification_email)
+ end
+ end
+end
+
shared_examples 'an email that contains a header with author username' do
it 'has X-GitLab-Author header containing author\'s username' do
is_expected.to have_header 'X-GitLab-Author', user.username
@@ -252,3 +295,31 @@ shared_examples 'a note email' do
end
end
end
+
+shared_examples 'appearance header and footer enabled' do
+ it "contains header and footer" do
+ create :appearance, header_message: "Foo", footer_message: "Bar", email_header_and_footer_enabled: true
+
+ aggregate_failures do
+ expect(subject.html_part).to have_body_text("<div class=\"header-message\" style=\"\"><p>Foo</p></div>")
+ expect(subject.html_part).to have_body_text("<div class=\"footer-message\" style=\"\"><p>Bar</p></div>")
+
+ expect(subject.text_part).to have_body_text(/^Foo/)
+ expect(subject.text_part).to have_body_text(/Bar$/)
+ end
+ end
+end
+
+shared_examples 'appearance header and footer not enabled' do
+ it "does not contain header and footer" do
+ create :appearance, header_message: "Foo", footer_message: "Bar", email_header_and_footer_enabled: false
+
+ aggregate_failures do
+ expect(subject.html_part).not_to have_body_text("<div class=\"header-message\" style=\"\"><p>Foo</p></div>")
+ expect(subject.html_part).not_to have_body_text("<div class=\"footer-message\" style=\"\"><p>Bar</p></div>")
+
+ expect(subject.text_part).not_to have_body_text(/^Foo/)
+ expect(subject.text_part).not_to have_body_text(/Bar$/)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
new file mode 100644
index 00000000000..7a71e2ee370
--- /dev/null
+++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb
@@ -0,0 +1,231 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'archived project policies' do
+ let(:feature_write_abilities) do
+ described_class::READONLY_FEATURES_WHEN_ARCHIVED.flat_map do |feature|
+ described_class.create_update_admin_destroy(feature)
+ end + additional_reporter_permissions + additional_maintainer_permissions
+ end
+
+ let(:other_write_abilities) do
+ %i[
+ create_merge_request_in
+ create_merge_request_from
+ push_to_delete_protected_branch
+ push_code
+ request_access
+ upload_file
+ resolve_note
+ award_emoji
+ ]
+ end
+
+ context 'when the project is archived' do
+ before do
+ project.archived = true
+ end
+
+ it 'disables write actions on all relevant project features' do
+ expect_disallowed(*feature_write_abilities)
+ end
+
+ it 'disables some other important write actions' do
+ expect_disallowed(*other_write_abilities)
+ end
+
+ it 'does not disable other abilities' do
+ expect_allowed(*(regular_abilities - feature_write_abilities - other_write_abilities))
+ end
+ end
+end
+
+RSpec.shared_examples 'project policies as anonymous' do
+ context 'abilities for public projects' do
+ context 'when a project has pending invites' do
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, namespace: group) }
+ let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] }
+ let(:anonymous_permissions) { guest_permissions - user_permissions }
+
+ subject { described_class.new(nil, project) }
+
+ before do
+ create(:group_member, :invited, group: group)
+ end
+
+ it 'does not grant owner access' do
+ expect_allowed(*anonymous_permissions)
+ expect_disallowed(*user_permissions)
+ end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { anonymous_permissions }
+ end
+ end
+ end
+
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(nil, project) }
+
+ it { is_expected.to be_banned }
+ end
+end
+
+RSpec.shared_examples 'project policies as guest' do
+ subject { described_class.new(guest, project) }
+
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+ let(:reporter_public_build_permissions) do
+ reporter_permissions - [:read_build, :read_pipeline]
+ end
+
+ it do
+ expect_allowed(*guest_permissions)
+ expect_disallowed(*reporter_public_build_permissions)
+ expect_disallowed(*team_member_reporter_permissions)
+ expect_disallowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { guest_permissions }
+ end
+
+ context 'public builds enabled' do
+ it do
+ expect_allowed(*guest_permissions)
+ expect_allowed(:read_build, :read_pipeline)
+ end
+ end
+
+ context 'when public builds disabled' do
+ before do
+ project.update(public_builds: false)
+ end
+
+ it do
+ expect_allowed(*guest_permissions)
+ expect_disallowed(:read_build, :read_pipeline)
+ end
+ end
+
+ context 'when builds are disabled' do
+ before do
+ project.project_feature.update(builds_access_level: ProjectFeature::DISABLED)
+ end
+
+ it do
+ expect_disallowed(:read_build)
+ expect_allowed(:read_pipeline)
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'project policies as reporter' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(reporter, project) }
+
+ it do
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_allowed(*team_member_reporter_permissions)
+ expect_disallowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { reporter_permissions }
+ end
+ end
+end
+
+RSpec.shared_examples 'project policies as developer' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+ subject { described_class.new(developer, project) }
+
+ it do
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_allowed(*team_member_reporter_permissions)
+ expect_allowed(*developer_permissions)
+ expect_disallowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { developer_permissions }
+ end
+ end
+end
+
+RSpec.shared_examples 'project policies as maintainer' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(maintainer, project) }
+
+ it do
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_allowed(*team_member_reporter_permissions)
+ expect_allowed(*developer_permissions)
+ expect_allowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { maintainer_permissions }
+ end
+ end
+end
+
+RSpec.shared_examples 'project policies as owner' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(owner, project) }
+
+ it do
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_allowed(*team_member_reporter_permissions)
+ expect_allowed(*developer_permissions)
+ expect_allowed(*maintainer_permissions)
+ expect_allowed(*owner_permissions)
+ end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { owner_permissions }
+ end
+ end
+end
+
+RSpec.shared_examples 'project policies as admin' do
+ context 'abilities for non-public projects' do
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ subject { described_class.new(admin, project) }
+
+ it do
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_disallowed(*team_member_reporter_permissions)
+ expect_allowed(*developer_permissions)
+ expect_allowed(*maintainer_permissions)
+ expect_allowed(*owner_permissions)
+ end
+
+ it_behaves_like 'archived project policies' do
+ let(:regular_abilities) { owner_permissions }
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..b337a1c18d8
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/commit/tag_quick_action_shared_examples.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+shared_examples 'tag quick action' do
+ context "post note to existing commit" do
+ it 'tags this commit' do
+ add_note("/tag #{tag_name} #{tag_message}")
+
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content "tagged commit #{truncated_commit_sha}"
+ expect(page).to have_content tag_name
+
+ visit project_tag_path(project, tag_name)
+ expect(page).to have_content tag_name
+ expect(page).to have_content tag_message
+ expect(page).to have_content truncated_commit_sha
+ end
+ end
+
+ context 'preview', :js do
+ it 'removes quick action from note and explains it' do
+ preview_note("/tag #{tag_name} #{tag_message}")
+
+ expect(page).not_to have_content '/tag'
+ expect(page).to have_content %{Tags this commit to #{tag_name} with "#{tag_message}"}
+ expect(page).to have_content tag_name
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..d97da6be192
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/assign_quick_action_shared_examples.rb
@@ -0,0 +1,110 @@
+# frozen_string_literal: true
+
+shared_examples 'assign quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets assign quick action accordingly" do
+ assignee = create(:user, username: 'bob')
+
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/assign @bob"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable.assignees).to eq [assignee]
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+
+ it "creates the #{issuable_type} and interprets assign quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/assign me"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable.assignees).to eq [maintainer]
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the assign quick action accordingly' do
+ assignee = create(:user, username: 'bob')
+ add_note("Awesome!\n\n/assign @bob")
+
+ expect(page).to have_content 'Awesome!'
+ expect(page).not_to have_content '/assign @bob'
+
+ wait_for_requests
+ issuable.reload
+ note = issuable.notes.user.first
+
+ expect(note.note).to eq 'Awesome!'
+ expect(issuable.assignees).to eq [assignee]
+ end
+
+ it "assigns the #{issuable_type} to the current user" do
+ add_note("/assign me")
+
+ expect(page).not_to have_content '/assign me'
+ expect(page).to have_content 'Commands applied'
+
+ expect(issuable.reload.assignees).to eq [maintainer]
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains assign quick action to bob' do
+ create(:user, username: 'bob')
+
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "Awesome!\n/assign @bob "
+ click_on 'Preview'
+
+ expect(page).not_to have_content '/assign @bob'
+ expect(page).to have_content 'Awesome!'
+ expect(page).to have_content 'Assigns @bob.'
+ end
+ end
+
+ it 'explains assign quick action to me' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "Awesome!\n/assign me"
+ click_on 'Preview'
+
+ expect(page).not_to have_content '/assign me'
+ expect(page).to have_content 'Awesome!'
+ expect(page).to have_content "Assigns @#{maintainer.username}."
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..74cbfa3f4b4
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/award_quick_action_shared_examples.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+shared_examples 'award quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets award quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/award :100:"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.award_emoji).to eq []
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ expect(issuable.award_emoji).to eq []
+ end
+
+ it 'creates the note and interprets the award quick action accordingly' do
+ add_note("/award :100:")
+
+ wait_for_requests
+ expect(page).not_to have_content '/award'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.award_emoji.last.name).to eq('100')
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains label quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/award :100:')
+
+ expect(page).not_to have_content '/award'
+ expect(page).to have_selector "gl-emoji[data-name='100']"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..a79a61bc708
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+shared_examples 'close quick action' do |issuable_type|
+ include Spec::Support::Helpers::Features::NotesHelpers
+
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets close quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/close"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ expect(issuable).to be_opened
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the close quick action accordingly' do
+ add_note("this is done, close\n\n/close")
+
+ wait_for_requests
+ expect(page).not_to have_content '/close'
+ expect(page).to have_content 'this is done, close'
+
+ issuable.reload
+ note = issuable.notes.user.first
+
+ expect(note.note).to eq 'this is done, close'
+ expect(issuable).to be_closed
+ end
+
+ context "when current user cannot close #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ end
+
+ it "does not close the #{issuable_type}" do
+ add_note('/close')
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(issuable).to be_open
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains close quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note("this is done, close\n/close") do
+ expect(page).not_to have_content '/close'
+ expect(page).to have_content 'this is done, close'
+ expect(page).to have_content "Closes this #{issuable_type.to_s.humanize.downcase}."
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..1e1e3c7bc95
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/copy_metadata_quick_action_shared_examples.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+shared_examples 'copy_metadata quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets copy_metadata quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/copy_metadata #{source_issuable.to_reference(project)}"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).last
+
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ issuable.reload
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable.milestone).to eq milestone
+ expect(issuable.labels).to match_array([label_bug, label_feature])
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets copy_metadata quick action accordingly' do
+ add_note("/copy_metadata #{source_issuable.to_reference(project)}")
+
+ wait_for_requests
+ expect(page).not_to have_content '/copy_metadata'
+ expect(page).to have_content 'Commands applied'
+ issuable.reload
+ expect(issuable.milestone).to eq milestone
+ expect(issuable.labels).to match_array([label_bug, label_feature])
+ end
+
+ context "when current user cannot copy_metadata" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not copy_metadata' do
+ add_note("/copy_metadata #{source_issuable.to_reference(project)}")
+
+ wait_for_requests
+ expect(page).not_to have_content '/copy_metadata'
+ expect(page).not_to have_content 'Commands applied'
+ issuable.reload
+ expect(issuable.milestone).not_to eq milestone
+ expect(issuable.labels).to eq []
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains copy_metadata quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note("/copy_metadata #{source_issuable.to_reference(project)}")
+
+ expect(page).not_to have_content '/copy_metadata'
+ expect(page).to have_content "Copy labels and milestone from #{source_issuable.to_reference(project)}."
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..8a72bbc13bf
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/done_quick_action_shared_examples.rb
@@ -0,0 +1,103 @@
+# frozen_string_literal: true
+
+shared_examples 'done quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets done quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/done"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+
+ todos = TodosFinder.new(maintainer).execute
+ expect(todos.size).to eq 0
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ TodoService.new.mark_todo(issuable, maintainer)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the done quick action accordingly' do
+ todos = TodosFinder.new(maintainer).execute
+ todo = todos.first
+ expect(todo.reload).to be_pending
+
+ expect(todos.size).to eq 1
+ expect(todo.target).to eq issuable
+ expect(todo.author).to eq maintainer
+ expect(todo.user).to eq maintainer
+
+ add_note('/done')
+
+ wait_for_requests
+ expect(page).not_to have_content '/done'
+ expect(page).to have_content 'Commands applied'
+ expect(todo.reload).to be_done
+ end
+
+ context "when current user cannot mark #{issuable_type} todo as done" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it "does not set the #{issuable_type} todo as done" do
+ todos = TodosFinder.new(maintainer).execute
+ todo = todos.first
+ expect(todo.reload).to be_pending
+
+ expect(todos.size).to eq 1
+ expect(todo.target).to eq issuable
+ expect(todo.author).to eq maintainer
+ expect(todo.user).to eq maintainer
+
+ add_note('/done')
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(todo.reload).to be_pending
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains done quick action' do
+ TodoService.new.mark_todo(issuable, maintainer)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/done')
+
+ expect(page).not_to have_content '/done'
+ expect(page).to have_content "Marks todo as done."
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..648755d7e55
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/estimate_quick_action_shared_examples.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+shared_examples 'estimate quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets estimate quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/estimate 1d 2h 3m"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.time_estimate).to eq 36180
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the estimate quick action accordingly' do
+ add_note("/estimate 1d 2h 3m")
+
+ wait_for_requests
+ expect(page).not_to have_content '/estimate'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.time_estimate).to eq 36180
+ end
+
+ context "when current user cannot set estimate to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not set estimate' do
+ add_note("/estimate ~bug ~feature")
+
+ wait_for_requests
+ expect(page).not_to have_content '/estimate'
+ expect(issuable.reload.time_estimate).to eq 0
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains estimate quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/estimate 1d 2h 3m')
+
+ expect(page).not_to have_content '/estimate'
+ expect(page).to have_content 'Sets time estimate to 1d 2h 3m.'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..9066e382b70
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/label_quick_action_shared_examples.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+shared_examples 'label quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets label quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug ~feature"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.labels).to match_array([label_bug, label_feature])
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ expect(issuable.labels).to eq []
+ end
+
+ it 'creates the note and interprets the label quick action accordingly' do
+ add_note("/label ~bug ~feature")
+
+ wait_for_requests
+ expect(page).not_to have_content '/label'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.labels).to match_array([label_bug, label_feature])
+ end
+
+ context "when current user cannot set label to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not set label' do
+ add_note("/label ~bug ~feature")
+
+ wait_for_requests
+ expect(page).not_to have_content '/label'
+ expect(issuable.labels).to eq []
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains label quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/label ~bug ~feature')
+
+ expect(page).not_to have_content '/label'
+ expect(page).to have_content 'Adds bug feature labels.'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..d3197f2a459
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/lock_quick_action_shared_examples.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+shared_examples 'lock quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets lock quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/lock"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable).not_to be_discussion_locked
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ issuable.update(discussion_locked: false)
+ expect(issuable).not_to be_discussion_locked
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the lock quick action accordingly' do
+ add_note('/lock')
+
+ wait_for_requests
+ expect(page).not_to have_content '/lock'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload).to be_discussion_locked
+ end
+
+ context "when current user cannot lock to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it "does not lock the #{issuable_type}" do
+ add_note('/lock')
+
+ wait_for_requests
+ expect(page).not_to have_content '/lock'
+ expect(issuable).not_to be_discussion_locked
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains lock quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/lock')
+
+ expect(page).not_to have_content '/lock'
+ expect(page).to have_content "Locks the discussion"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..7f16ce93b6a
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/milestone_quick_action_shared_examples.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+shared_examples 'milestone quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets milestone quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/milestone %\"ASAP\""
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.milestone).to eq milestone
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ expect(issuable.milestone).to be_nil
+ end
+
+ it 'creates the note and interprets the milestone quick action accordingly' do
+ add_note("/milestone %\"ASAP\"")
+
+ wait_for_requests
+ expect(page).not_to have_content '/milestone'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.milestone).to eq milestone
+ end
+
+ context "when current user cannot set milestone to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not set milestone' do
+ add_note('/milestone')
+
+ wait_for_requests
+ expect(page).not_to have_content '/milestone'
+ expect(issuable.reload.milestone).to be_nil
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains milestone quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note("/milestone %\"ASAP\"")
+
+ expect(page).not_to have_content '/milestone'
+ expect(page).to have_content 'Sets the milestone to %ASAP'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..643ae77516a
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/relabel_quick_action_shared_examples.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+shared_examples 'relabel quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets relabel quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug /relabel ~feature"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.labels).to eq [label_bug, label_feature]
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ issuable.update(labels: [label_bug])
+ end
+
+ it 'creates the note and interprets the relabel quick action accordingly' do
+ add_note('/relabel ~feature')
+
+ wait_for_requests
+ expect(page).not_to have_content '/relabel'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.labels).to match_array([label_feature])
+ end
+
+ it 'creates the note and interprets the relabel quick action with empty param' do
+ add_note('/relabel')
+
+ wait_for_requests
+ expect(page).not_to have_content '/relabel'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.labels).to match_array([label_bug])
+ end
+
+ context "when current user cannot relabel to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not relabel' do
+ add_note('/relabel ~feature')
+
+ wait_for_requests
+ expect(page).not_to have_content '/relabel'
+ expect(issuable.labels).to match_array([label_bug])
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ before do
+ issuable.update(labels: [label_bug])
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ end
+
+ it 'explains relabel all quick action' do
+ preview_note('/relabel ~feature')
+
+ expect(page).not_to have_content '/relabel'
+ expect(page).to have_content 'Replaces all labels with feature label.'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..24f6f8d5bf4
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/remove_estimate_quick_action_shared_examples.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+shared_examples 'remove_estimate quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets estimate quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/remove_estimate"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.time_estimate).to eq 0
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ issuable.update_attribute(:time_estimate, 36180)
+ end
+
+ it 'creates the note and interprets the remove_estimate quick action accordingly' do
+ add_note("/remove_estimate")
+
+ wait_for_requests
+ expect(page).not_to have_content '/remove_estimate'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.time_estimate).to eq 0
+ end
+
+ context "when current user cannot remove_estimate" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not remove_estimate' do
+ add_note('/remove_estimate')
+
+ wait_for_requests
+ expect(page).not_to have_content '/remove_estimate'
+ expect(issuable.reload.time_estimate).to eq 36180
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains remove_estimate quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/remove_estimate')
+
+ expect(page).not_to have_content '/remove_estimate'
+ expect(page).to have_content 'Removes time estimate.'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..edd92d5cdbc
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/remove_milestone_quick_action_shared_examples.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+shared_examples 'remove_milestone quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets remove_milestone quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/remove_milestone"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.milestone).to be_nil
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ issuable.update(milestone: milestone)
+ expect(issuable.milestone).to eq(milestone)
+ end
+
+ it 'creates the note and interprets the remove_milestone quick action accordingly' do
+ add_note("/remove_milestone")
+
+ wait_for_requests
+ expect(page).not_to have_content '/remove_milestone'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.milestone).to be_nil
+ end
+
+ context "when current user cannot remove milestone to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not remove milestone' do
+ add_note('/remove_milestone')
+
+ wait_for_requests
+ expect(page).not_to have_content '/remove_milestone'
+ expect(issuable.reload.milestone).to eq(milestone)
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains remove_milestone quick action' do
+ issuable.update(milestone: milestone)
+ expect(issuable.milestone).to eq(milestone)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note("/remove_milestone")
+
+ expect(page).not_to have_content '/remove_milestone'
+ expect(page).to have_content 'Removes %ASAP milestone.'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..6d5894b2318
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/remove_time_spent_quick_action_shared_examples.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+shared_examples 'remove_time_spent quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets remove_time_spent quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/remove_time_spent"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.total_time_spent).to eq 0
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ issuable.update!(spend_time: { duration: 36180, user_id: maintainer.id })
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the remove_time_spent quick action accordingly' do
+ add_note("/remove_time_spent")
+
+ wait_for_requests
+ expect(page).not_to have_content '/remove_time_spent'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.total_time_spent).to eq 0
+ end
+
+ context "when current user cannot set remove_time_spent time" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not set remove_time_spent time' do
+ add_note("/remove_time_spent")
+
+ wait_for_requests
+ expect(page).not_to have_content '/remove_time_spent'
+ expect(issuable.reload.total_time_spent).to eq 36180
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains remove_time_spent quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/remove_time_spent')
+
+ expect(page).not_to have_content '/remove_time_spent'
+ expect(page).to have_content 'Removes spent time.'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..af173e93bb5
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/reopen_quick_action_shared_examples.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+shared_examples 'reopen quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets reopen quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/reopen"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ issuable.close
+ expect(issuable).to be_closed
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the reopen quick action accordingly' do
+ add_note('/reopen')
+
+ wait_for_requests
+ expect(page).not_to have_content '/reopen'
+ expect(page).to have_content 'Commands applied'
+
+ issuable.reload
+ expect(issuable).to be_opened
+ end
+
+ context "when current user cannot reopen #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it "does not reopen the #{issuable_type}" do
+ add_note('/reopen')
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(issuable).to be_closed
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains reopen quick action' do
+ issuable.close
+ expect(issuable).to be_closed
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/reopen')
+
+ expect(page).not_to have_content '/reopen'
+ expect(page).to have_content "Reopens this #{issuable_type.to_s.humanize.downcase}."
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..0a526808585
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/shrug_quick_action_shared_examples.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+shared_examples 'shrug quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets shrug quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/shrug oops"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq "bug description\noops ¯\\_(ツ)_/¯"
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content "bug description\noops ¯\\_(ツ)_/¯"
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets shrug quick action accordingly' do
+ add_note("/shrug oops")
+
+ wait_for_requests
+ expect(page).not_to have_content '/shrug oops'
+ expect(page).to have_content "oops ¯\\_(ツ)_/¯"
+ expect(issuable.notes.last.note).to eq "oops ¯\\_(ツ)_/¯"
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains shrug quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/shrug oops')
+
+ expect(page).not_to have_content '/shrug'
+ expect(page).to have_content "oops ¯\\_(ツ)_/¯"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..97b4885eba0
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/spend_quick_action_shared_examples.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+shared_examples 'spend quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets spend quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/spend 1d 2h 3m"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.total_time_spent).to eq 36180
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the spend quick action accordingly' do
+ add_note("/spend 1d 2h 3m")
+
+ wait_for_requests
+ expect(page).not_to have_content '/spend'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.total_time_spent).to eq 36180
+ end
+
+ context "when current user cannot set spend time" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not set spend time' do
+ add_note("/spend 1s 2h 3m")
+
+ wait_for_requests
+ expect(page).not_to have_content '/spend'
+ expect(issuable.reload.total_time_spent).to eq 0
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains spend quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/spend 1d 2h 3m')
+
+ expect(page).not_to have_content '/spend'
+ expect(page).to have_content 'Adds 1d 2h 3m spent time.'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..15aefd511a5
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/subscribe_quick_action_shared_examples.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+shared_examples 'subscribe quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets subscribe quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/subscribe"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.subscribed?(maintainer, project)).to be_truthy
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ expect(issuable.subscribed?(maintainer, project)).to be_falsy
+ end
+
+ it 'creates the note and interprets the subscribe quick action accordingly' do
+ add_note('/subscribe')
+
+ wait_for_requests
+ expect(page).not_to have_content '/subscribe'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.subscribed?(maintainer, project)).to be_truthy
+ end
+
+ context "when current user cannot subscribe to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it "does not subscribe to the #{issuable_type}" do
+ add_note('/subscribe')
+
+ wait_for_requests
+ expect(page).not_to have_content '/subscribe'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.subscribed?(maintainer, project)).to be_falsy
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains subscribe quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/subscribe')
+
+ expect(page).not_to have_content '/subscribe'
+ expect(page).to have_content "Subscribes to this #{issuable_type.to_s.humanize.downcase}"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..ef831e39872
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/tableflip_quick_action_shared_examples.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+shared_examples 'tableflip quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets tableflip quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/tableflip oops"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq "bug description\noops (╯°□°)╯︵ ┻━┻"
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content "bug description\noops (╯°□°)╯︵ ┻━┻"
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets tableflip quick action accordingly' do
+ add_note("/tableflip oops")
+
+ wait_for_requests
+ expect(page).not_to have_content '/tableflip oops'
+ expect(page).to have_content "oops (╯°□°)╯︵ ┻━┻"
+ expect(issuable.notes.last.note).to eq "oops (╯°□°)╯︵ ┻━┻"
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains tableflip quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/tableflip oops')
+
+ expect(page).not_to have_content '/tableflip'
+ expect(page).to have_content "oops (╯°□°)╯︵ ┻━┻"
+ end
+ end
+end
diff --git a/spec/support/shared_examples/time_tracking_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
index 909d4e2ee8d..ed904c8d539 100644
--- a/spec/support/shared_examples/time_tracking_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/time_tracking_quick_action_shared_examples.rb
@@ -1,4 +1,17 @@
-shared_examples 'issuable time tracker' do
+# frozen_string_literal: true
+
+shared_examples 'issuable time tracker' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ after do
+ wait_for_requests
+ end
+
it 'renders the sidebar component empty state' do
page.within '.time-tracking-no-tracking-pane' do
expect(page).to have_content 'No estimate or time spent'
diff --git a/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..93a69093dde
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/title_quick_action_shared_examples.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+shared_examples 'title quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets title quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/title new title"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(issuable.title).to eq 'bug 345'
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the title quick action accordingly' do
+ add_note('/title New title')
+
+ wait_for_requests
+ expect(page).not_to have_content '/title new title'
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content 'New title'
+
+ issuable.reload
+ expect(issuable.title).to eq 'New title'
+ end
+
+ context "when current user cannot set title #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it "does not set title to the #{issuable_type}" do
+ add_note('/title New title')
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(issuable.title).not_to eq 'New title'
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains title quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/title New title')
+ wait_for_requests
+
+ expect(page).not_to have_content '/title New title'
+ expect(page).to have_content 'Changes the title to "New title".'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..cccc28127ce
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/todo_quick_action_shared_examples.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+shared_examples 'todo quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets todo quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/todo"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+
+ todos = TodosFinder.new(maintainer).execute
+ expect(todos.size).to eq 0
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the todo quick action accordingly' do
+ add_note('/todo')
+
+ wait_for_requests
+ expect(page).not_to have_content '/todo'
+ expect(page).to have_content 'Commands applied'
+
+ todos = TodosFinder.new(maintainer).execute
+ todo = todos.first
+
+ expect(todos.size).to eq 1
+ expect(todo).to be_pending
+ expect(todo.target).to eq issuable
+ expect(todo.author).to eq maintainer
+ expect(todo.user).to eq maintainer
+ end
+
+ context "when current user cannot add todo #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it "does not add todo the #{issuable_type}" do
+ add_note('/todo')
+
+ expect(page).not_to have_content 'Commands applied'
+ todos = TodosFinder.new(maintainer).execute
+ expect(todos.size).to eq 0
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains todo quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/todo')
+
+ expect(page).not_to have_content '/todo'
+ expect(page).to have_content "Adds a todo."
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..0b1a52bc860
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/unassign_quick_action_shared_examples.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+shared_examples 'unassign quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets unassign quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/unassign @bob"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable.assignees).to eq []
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+
+ it "creates the #{issuable_type} and interprets unassign quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/unassign me"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable.assignees).to eq []
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the unassign quick action accordingly' do
+ assignee = create(:user, username: 'bob')
+ issuable.update(assignee_ids: [assignee.id])
+ expect(issuable.assignees).to eq [assignee]
+
+ add_note("Awesome!\n\n/unassign @bob")
+
+ expect(page).to have_content 'Awesome!'
+ expect(page).not_to have_content '/unassign @bob'
+
+ wait_for_requests
+ issuable.reload
+ note = issuable.notes.user.first
+
+ expect(note.note).to eq 'Awesome!'
+ expect(issuable.assignees).to eq []
+ end
+
+ it "unassigns the #{issuable_type} from current user" do
+ issuable.update(assignee_ids: [maintainer.id])
+ expect(issuable.reload.assignees).to eq [maintainer]
+ expect(issuable.assignees).to eq [maintainer]
+
+ add_note("/unassign me")
+
+ expect(page).not_to have_content '/unassign me'
+ expect(page).to have_content 'Commands applied'
+
+ expect(issuable.reload.assignees).to eq []
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains unassign quick action: from bob' do
+ assignee = create(:user, username: 'bob')
+ issuable.update(assignee_ids: [assignee.id])
+ expect(issuable.assignees).to eq [assignee]
+
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "Awesome!\n/unassign @bob "
+ click_on 'Preview'
+
+ expect(page).not_to have_content '/unassign @bob'
+ expect(page).to have_content 'Awesome!'
+ expect(page).to have_content 'Removes assignee @bob.'
+ end
+ end
+
+ it 'explains unassign quick action: from me' do
+ issuable.update(assignee_ids: [maintainer.id])
+ expect(issuable.assignees).to eq [maintainer]
+
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "Awesome!\n/unassign me"
+ click_on 'Preview'
+
+ expect(page).not_to have_content '/unassign me'
+ expect(page).to have_content 'Awesome!'
+ expect(page).to have_content "Removes assignee @#{maintainer.username}."
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..1a1ee05841f
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/unlabel_quick_action_shared_examples.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+shared_examples 'unlabel quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets unlabel quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/label ~bug /unlabel"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.labels).to eq [label_bug]
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ issuable.update(labels: [label_bug, label_feature])
+ end
+
+ it 'creates the note and interprets the unlabel all quick action accordingly' do
+ add_note("/unlabel")
+
+ wait_for_requests
+ expect(page).not_to have_content '/unlabel'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.labels).to eq []
+ end
+
+ it 'creates the note and interprets the unlabel some quick action accordingly' do
+ add_note("/unlabel ~bug")
+
+ wait_for_requests
+ expect(page).not_to have_content '/unlabel'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload.labels).to match_array([label_feature])
+ end
+
+ context "when current user cannot unlabel to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'does not unlabel' do
+ add_note("/unlabel")
+
+ wait_for_requests
+ expect(page).not_to have_content '/unlabel'
+ expect(issuable.labels).to match_array([label_bug, label_feature])
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ before do
+ issuable.update(labels: [label_bug, label_feature])
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ end
+
+ it 'explains unlabel all quick action' do
+ preview_note('/unlabel')
+
+ expect(page).not_to have_content '/unlabel'
+ expect(page).to have_content 'Removes all labels.'
+ end
+
+ it 'explains unlabel some quick action' do
+ preview_note('/unlabel ~bug')
+
+ expect(page).not_to have_content '/unlabel'
+ expect(page).to have_content 'Removes bug label.'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..998ff99b32e
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/unlock_quick_action_shared_examples.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+shared_examples 'unlock quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets unlock quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/unlock"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable).not_to be_discussion_locked
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ issuable.update(discussion_locked: true)
+ expect(issuable).to be_discussion_locked
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it 'creates the note and interprets the unlock quick action accordingly' do
+ add_note('/unlock')
+
+ wait_for_requests
+ expect(page).not_to have_content '/unlock'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.reload).not_to be_discussion_locked
+ end
+
+ context "when current user cannot unlock to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it "does not lock the #{issuable_type}" do
+ add_note('/unlock')
+
+ wait_for_requests
+ expect(page).not_to have_content '/unlock'
+ expect(issuable).to be_discussion_locked
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains unlock quick action' do
+ issuable.update(discussion_locked: true)
+ expect(issuable).to be_discussion_locked
+
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+
+ preview_note('/unlock')
+
+ expect(page).not_to have_content '/unlock'
+ expect(page).to have_content 'Unlocks the discussion'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..bd92f133889
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issuable/unsubscribe_quick_action_shared_examples.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+shared_examples 'unsubscribe quick action' do |issuable_type|
+ before do
+ project.add_maintainer(maintainer)
+ gitlab_sign_in(maintainer)
+ end
+
+ context "new #{issuable_type}", :js do
+ before do
+ case issuable_type
+ when :merge_request
+ visit public_send('namespace_project_new_merge_request_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ when :issue
+ visit public_send('new_namespace_project_issue_path', project.namespace, project, new_url_opts)
+ wait_for_all_requests
+ end
+ end
+
+ it "creates the #{issuable_type} and interprets unsubscribe quick action accordingly" do
+ fill_in "#{issuable_type}_title", with: 'bug 345'
+ fill_in "#{issuable_type}_description", with: "bug description\n/unsubscribe"
+ click_button "Submit #{issuable_type}".humanize
+
+ issuable = project.public_send(issuable_type.to_s.pluralize).first
+
+ expect(issuable.description).to eq 'bug description'
+ expect(issuable).to be_opened
+ expect(page).to have_content 'bug 345'
+ expect(page).to have_content 'bug description'
+ expect(issuable.subscribed?(maintainer, project)).to be_truthy
+ end
+ end
+
+ context "post note to existing #{issuable_type}" do
+ before do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ issuable.subscribe(maintainer, project)
+ expect(issuable.subscribed?(maintainer, project)).to be_truthy
+ end
+
+ it 'creates the note and interprets the unsubscribe quick action accordingly' do
+ add_note('/unsubscribe')
+
+ wait_for_requests
+ expect(page).not_to have_content '/unsubscribe'
+ expect(page).to have_content 'Commands applied'
+ expect(issuable.subscribed?(maintainer, project)).to be_falsey
+ end
+
+ context "when current user cannot unsubscribe to #{issuable_type}" do
+ before do
+ guest = create(:user)
+ project.add_guest(guest)
+
+ gitlab_sign_out
+ gitlab_sign_in(guest)
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ wait_for_all_requests
+ end
+
+ it "does not unsubscribe to the #{issuable_type}" do
+ add_note('/unsubscribe')
+
+ wait_for_requests
+ expect(page).not_to have_content '/unsubscribe'
+ expect(issuable.subscribed?(maintainer, project)).to be_truthy
+ end
+ end
+ end
+
+ context "preview of note on #{issuable_type}", :js do
+ it 'explains unsubscribe quick action' do
+ visit public_send("project_#{issuable_type}_path", project, issuable)
+ issuable.subscribe(maintainer, project)
+ expect(issuable.subscribed?(maintainer, project)).to be_truthy
+
+ preview_note('/unsubscribe')
+
+ expect(page).not_to have_content '/unsubscribe'
+ expect(page).to have_content "Unsubscribes from this #{issuable_type.to_s.humanize.downcase}."
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..6edd20bb024
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/board_move_quick_action_shared_examples.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+
+shared_examples 'board_move quick action' do
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..336500487fe
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/confidential_quick_action_shared_examples.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+shared_examples 'confidential quick action' do
+ context 'when the current user can update issues' do
+ it 'does not create a note, and marks the issue as confidential' do
+ add_note('/confidential')
+
+ expect(page).not_to have_content '/confidential'
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content 'made the issue confidential'
+
+ expect(issue.reload).to be_confidential
+ end
+ end
+
+ context 'when the current user cannot update the issue' do
+ let(:guest) { create(:user) }
+
+ before do
+ project.add_guest(guest)
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not create a note, and does not mark the issue as confidential' do
+ add_note('/confidential')
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content 'made the issue confidential'
+
+ expect(issue.reload).not_to be_confidential
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..34dba5dbc31
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/create_merge_request_quick_action_shared_examples.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+shared_examples 'create_merge_request quick action' do
+ context 'create a merge request starting from an issue' do
+ def expect_mr_quickaction(success)
+ expect(page).to have_content 'Commands applied'
+
+ if success
+ expect(page).to have_content 'created merge request'
+ else
+ expect(page).not_to have_content 'created merge request'
+ end
+ end
+
+ it "doesn't create a merge request when the branch name is invalid" do
+ add_note("/create_merge_request invalid branch name")
+
+ wait_for_requests
+
+ expect_mr_quickaction(false)
+ end
+
+ it "doesn't create a merge request when a branch with that name already exists" do
+ add_note("/create_merge_request feature")
+
+ wait_for_requests
+
+ expect_mr_quickaction(false)
+ end
+
+ it 'creates a new merge request using issue iid and title as branch name when the branch name is empty' do
+ add_note("/create_merge_request")
+
+ wait_for_requests
+
+ expect_mr_quickaction(true)
+
+ created_mr = project.merge_requests.last
+ expect(created_mr.source_branch).to eq(issue.to_branch_name)
+
+ visit project_merge_request_path(project, created_mr)
+ expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
+ end
+
+ it 'creates a merge request using the given branch name' do
+ branch_name = '1-feature'
+ add_note("/create_merge_request #{branch_name}")
+
+ expect_mr_quickaction(true)
+
+ created_mr = project.merge_requests.last
+ expect(created_mr.source_branch).to eq(branch_name)
+
+ visit project_merge_request_path(project, created_mr)
+ expect(page).to have_content %{WIP: Resolve "#{issue.title}"}
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..ae78cd86cd5
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/due_quick_action_shared_examples.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+shared_examples 'due quick action' do
+ context 'due quick action available and date can be added' do
+ it 'sets the due date accordingly' do
+ add_note('/due 2016-08-28')
+
+ expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).to have_content 'Commands applied'
+
+ visit project_issue_path(project, issue)
+
+ page.within '.due_date' do
+ expect(page).to have_content 'Aug 28, 2016'
+ end
+ end
+ end
+
+ context 'due quick action not available' do
+ let(:guest) { create(:user) }
+ before do
+ project.add_guest(guest)
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not set the due date' do
+ add_note('/due 2016-08-28')
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content '/due 2016-08-28'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..633c7135fbc
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/duplicate_quick_action_shared_examples.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+shared_examples 'duplicate quick action' do
+ context 'mark issue as duplicate' do
+ let(:original_issue) { create(:issue, project: project) }
+
+ context 'when the current user can update issues' do
+ it 'does not create a note, and marks the issue as a duplicate' do
+ add_note("/duplicate ##{original_issue.to_reference}")
+
+ expect(page).not_to have_content "/duplicate #{original_issue.to_reference}"
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
+
+ expect(issue.reload).to be_closed
+ end
+ end
+
+ context 'when the current user cannot update the issue' do
+ let(:guest) { create(:user) }
+ before do
+ project.add_guest(guest)
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not create a note, and does not mark the issue as a duplicate' do
+ add_note("/duplicate ##{original_issue.to_reference}")
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
+
+ expect(issue.reload).to be_open
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..a0b0d888769
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/move_quick_action_shared_examples.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+shared_examples 'move quick action' do
+ context 'move the issue to another project' do
+ let(:target_project) { create(:project, :public) }
+
+ context 'when the project is valid' do
+ before do
+ target_project.add_maintainer(user)
+ end
+
+ it 'moves the issue' do
+ add_note("/move #{target_project.full_path}")
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_closed
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'Issues 1'
+ end
+ end
+
+ context 'when the project is valid but the user not authorized' do
+ let(:project_unauthorized) { create(:project, :public) }
+
+ it 'does not move the issue' do
+ add_note("/move #{project_unauthorized.full_path}")
+
+ wait_for_requests
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_open
+ end
+ end
+
+ context 'when the project is invalid' do
+ it 'does not move the issue' do
+ add_note("/move not/valid")
+
+ wait_for_requests
+
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_open
+ end
+ end
+
+ context 'when the user issues multiple commands' do
+ let(:milestone) { create(:milestone, title: '1.0', project: project) }
+ let(:bug) { create(:label, project: project, title: 'bug') }
+ let(:wontfix) { create(:label, project: project, title: 'wontfix') }
+
+ before do
+ target_project.add_maintainer(user)
+ end
+
+ shared_examples 'applies the commands to issues in both projects, target and source' do
+ it "applies quick actions" do
+ expect(page).to have_content 'Commands applied'
+ expect(issue.reload).to be_closed
+
+ visit project_issue_path(target_project, issue)
+
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+
+ visit project_issue_path(project, issue)
+ expect(page).to have_content 'Closed'
+ expect(page).to have_content 'bug'
+ expect(page).to have_content 'wontfix'
+ expect(page).to have_content '1.0'
+ end
+ end
+
+ context 'applies multiple commands with move command in the end' do
+ before do
+ add_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
+ end
+
+ it_behaves_like 'applies the commands to issues in both projects, target and source'
+ end
+
+ context 'applies multiple commands with move command in the begining' do
+ before do
+ add_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
+ end
+
+ it_behaves_like 'applies the commands to issues in both projects, target and source'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..657c2a60d24
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/issue/remove_due_date_quick_action_shared_examples.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+shared_examples 'remove_due_date quick action' do
+ context 'remove_due_date action available and due date can be removed' do
+ it 'removes the due date accordingly' do
+ add_note('/remove_due_date')
+
+ expect(page).not_to have_content '/remove_due_date'
+ expect(page).to have_content 'Commands applied'
+
+ visit project_issue_path(project, issue)
+
+ page.within '.due_date' do
+ expect(page).to have_content 'None'
+ end
+ end
+ end
+
+ context 'remove_due_date action not available' do
+ let(:guest) { create(:user) }
+ before do
+ project.add_guest(guest)
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not remove the due date' do
+ add_note("/remove_due_date")
+
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content '/remove_due_date'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..c454ddc4bba
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/merge_request/merge_quick_action_shared_examples.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+shared_examples 'merge quick action' do
+ context 'when the current user can merge the MR' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'merges the MR' do
+ add_note("/merge")
+
+ expect(page).to have_content 'Commands applied'
+
+ expect(merge_request.reload).to be_merged
+ end
+ end
+
+ context 'when the head diff changes in the meanwhile' do
+ before do
+ merge_request.source_branch = 'another_branch'
+ merge_request.save
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not merge the MR' do
+ add_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+
+ context 'when the current user cannot merge the MR' do
+ before do
+ project.add_guest(guest)
+ sign_in(guest)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not merge the MR' do
+ add_note("/merge")
+
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload).not_to be_merged
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..cf2bdb1dd68
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/merge_request/target_branch_quick_action_shared_examples.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+shared_examples 'target_branch quick action' do
+ describe '/target_branch command in merge request' do
+ let(:another_project) { create(:project, :public, :repository) }
+ let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
+
+ before do
+ another_project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ it 'changes target_branch in new merge_request' do
+ visit project_new_merge_request_path(another_project, new_url_opts)
+
+ fill_in "merge_request_title", with: 'My brand new feature'
+ fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
+ click_button "Submit merge request"
+
+ merge_request = another_project.merge_requests.first
+ expect(merge_request.description).to eq "le feature \nFeature description:"
+ expect(merge_request.target_branch).to eq 'fix'
+ end
+
+ it 'does not change target branch when merge request is edited' do
+ new_merge_request = create(:merge_request, source_project: another_project)
+
+ visit edit_project_merge_request_path(another_project, new_merge_request)
+ fill_in "merge_request_description", with: "Want to update target branch\n/target_branch fix\n"
+ click_button "Save changes"
+
+ new_merge_request = another_project.merge_requests.first
+ expect(new_merge_request.description).to include('/target_branch')
+ expect(new_merge_request.target_branch).not_to eq('fix')
+ end
+ end
+
+ describe '/target_branch command from note' do
+ context 'when the current user can change target branch' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'changes target branch from a note' do
+ add_note("message start \n/target_branch merge-test\n message end.")
+
+ wait_for_requests
+ expect(page).not_to have_content('/target_branch')
+ expect(page).to have_content('message start')
+ expect(page).to have_content('message end.')
+
+ expect(merge_request.reload.target_branch).to eq 'merge-test'
+ end
+
+ it 'does not fail when target branch does not exists' do
+ add_note('/target_branch totally_not_existing_branch')
+
+ expect(page).not_to have_content('/target_branch')
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+
+ context 'when current user can not change target branch' do
+ before do
+ project.add_guest(guest)
+ sign_in(guest)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not change target branch' do
+ add_note('/target_branch merge-test')
+
+ expect(page).not_to have_content '/target_branch merge-test'
+
+ expect(merge_request.target_branch).to eq 'feature'
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb
new file mode 100644
index 00000000000..60d6e53e74f
--- /dev/null
+++ b/spec/support/shared_examples/quick_actions/merge_request/wip_quick_action_shared_examples.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+shared_examples 'wip quick action' do
+ context 'when the current user can toggle the WIP prefix' do
+ before do
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ wait_for_requests
+ end
+
+ it 'adds the WIP: prefix to the title' do
+ add_note('/wip')
+
+ expect(page).not_to have_content '/wip'
+ expect(page).to have_content 'Commands applied'
+
+ expect(merge_request.reload.work_in_progress?).to eq true
+ end
+
+ it 'removes the WIP: prefix from the title' do
+ merge_request.update!(title: merge_request.wip_title)
+ add_note('/wip')
+
+ expect(page).not_to have_content '/wip'
+ expect(page).to have_content 'Commands applied'
+
+ expect(merge_request.reload.work_in_progress?).to eq false
+ end
+ end
+
+ context 'when the current user cannot toggle the WIP prefix' do
+ before do
+ project.add_guest(guest)
+ sign_in(guest)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'does not change the WIP prefix' do
+ add_note('/wip')
+
+ expect(page).not_to have_content '/wip'
+ expect(page).not_to have_content 'Commands applied'
+
+ expect(merge_request.reload.work_in_progress?).to eq false
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/discussions.rb b/spec/support/shared_examples/requests/api/discussions.rb
index e44da4faa5a..c3132c41f5b 100644
--- a/spec/support/shared_examples/requests/api/discussions.rb
+++ b/spec/support/shared_examples/requests/api/discussions.rb
@@ -1,4 +1,4 @@
-shared_examples 'discussions API' do |parent_type, noteable_type, id_name|
+shared_examples 'discussions API' do |parent_type, noteable_type, id_name, can_reply_to_individual_notes: false|
describe "GET /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions" do
it "returns an array of discussions" do
get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user)
@@ -86,6 +86,37 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name|
expect(response).to have_gitlab_http_status(404)
end
end
+
+ context 'when a project is public with private repo access' do
+ let!(:parent) { create(:project, :public, :repository, :repository_private, :snippets_private) }
+ let!(:user_without_access) { create(:user) }
+
+ context 'when user is not a team member of private repo' do
+ before do
+ project.team.truncate
+ end
+
+ context "creating a new note" do
+ before do
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions", user_without_access), params: { body: 'hi!' }
+ end
+
+ it 'raises 404 error' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context "fetching a discussion" do
+ before do
+ get api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/discussions/#{note.discussion_id}", user_without_access)
+ end
+
+ it 'raises 404 error' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+ end
end
describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/discussions/:discussion_id/notes" do
@@ -105,13 +136,25 @@ shared_examples 'discussions API' do |parent_type, noteable_type, id_name|
expect(response).to have_gitlab_http_status(400)
end
- it "returns a 400 bad request error if discussion is individual note" do
- note.update_attribute(:type, nil)
+ context 'when the discussion is an individual note' do
+ before do
+ note.update!(type: nil)
- post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
- "discussions/#{note.discussion_id}/notes", user), params: { body: 'hi!' }
+ post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/"\
+ "discussions/#{note.discussion_id}/notes", user), params: { body: 'hi!' }
+ end
- expect(response).to have_gitlab_http_status(400)
+ if can_reply_to_individual_notes
+ it 'creates a new discussion' do
+ expect(response).to have_gitlab_http_status(201)
+ expect(json_response['body']).to eq('hi!')
+ expect(json_response['type']).to eq('DiscussionNote')
+ end
+ else
+ it 'returns 400 bad request' do
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
end
end
diff --git a/spec/support/shared_examples/requests/api/issues_shared_examples.rb b/spec/support/shared_examples/requests/api/issues_shared_examples.rb
new file mode 100644
index 00000000000..1133e95e44e
--- /dev/null
+++ b/spec/support/shared_examples/requests/api/issues_shared_examples.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+shared_examples 'labeled issues with labels and label_name params' do
+ shared_examples 'returns label names' do
+ it 'returns label names' do
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels']).to eq([label_c.title, label_b.title, label.title])
+ end
+ end
+
+ shared_examples 'returns basic label entity' do
+ it 'returns basic label entity' do
+ expect_paginated_array_response(issue.id)
+ expect(json_response.first['labels'].pluck('name')).to eq([label_c.title, label_b.title, label.title])
+ expect(json_response.first['labels'].first).to match_schema('/public_api/v4/label_basic')
+ end
+ end
+
+ context 'array of labeled issues when all labels match' do
+ let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}" } }
+
+ it_behaves_like 'returns label names'
+ end
+
+ context 'array of labeled issues when all labels match with labels param as array' do
+ let(:params) { { labels: [label.title, label_b.title, label_c.title] } }
+
+ it_behaves_like 'returns label names'
+ end
+
+ context 'when with_labels_details provided' do
+ context 'array of labeled issues when all labels match' do
+ let(:params) { { labels: "#{label.title},#{label_b.title},#{label_c.title}", with_labels_details: true } }
+
+ it_behaves_like 'returns basic label entity'
+ end
+
+ context 'array of labeled issues when all labels match with labels param as array' do
+ let(:params) { { labels: [label.title, label_b.title, label_c.title], with_labels_details: true } }
+
+ it_behaves_like 'returns basic label entity'
+ end
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/merge_requests_list.rb b/spec/support/shared_examples/requests/api/merge_requests_list.rb
deleted file mode 100644
index 6713ec47ace..00000000000
--- a/spec/support/shared_examples/requests/api/merge_requests_list.rb
+++ /dev/null
@@ -1,335 +0,0 @@
-shared_examples 'merge requests list' do
- context 'when unauthenticated' do
- it 'returns merge requests for public projects' do
- get api(endpoint_path)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- end
- end
-
- context 'when authenticated' do
- it 'avoids N+1 queries' do
- control = ActiveRecord::QueryRecorder.new do
- get api(endpoint_path, user)
- end
-
- create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
-
- merge_request = create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: 'Test', created_at: base_time)
-
- merge_request.metrics.update!(merged_by: user,
- latest_closed_by: user,
- latest_closed_at: 1.hour.ago,
- merged_at: 2.hours.ago)
-
- expect do
- get api(endpoint_path, user)
- end.not_to exceed_query_limit(control)
- end
-
- it 'returns an array of all merge_requests' do
- get api(endpoint_path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- expect(json_response.last['title']).to eq(merge_request.title)
- expect(json_response.last).to have_key('web_url')
- expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
- expect(json_response.last['merge_commit_sha']).to be_nil
- expect(json_response.last['merge_commit_sha']).to eq(merge_request.merge_commit_sha)
- expect(json_response.last['downvotes']).to eq(1)
- expect(json_response.last['upvotes']).to eq(1)
- expect(json_response.last['labels']).to eq([label2.title, label.title])
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- expect(json_response.first['sha']).to eq(merge_request_merged.diff_head_sha)
- expect(json_response.first['merge_commit_sha']).not_to be_nil
- expect(json_response.first['merge_commit_sha']).to eq(merge_request_merged.merge_commit_sha)
- end
-
- it 'returns an array of all merge_requests using simple mode' do
- path = endpoint_path + '?view=simple'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- expect(json_response.last['iid']).to eq(merge_request.iid)
- expect(json_response.last['title']).to eq(merge_request.title)
- expect(json_response.last).to have_key('web_url')
- expect(json_response.first['iid']).to eq(merge_request_merged.iid)
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- expect(json_response.first).to have_key('web_url')
- end
-
- it 'returns an array of all merge_requests' do
- path = endpoint_path + '?state'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- expect(json_response.last['title']).to eq(merge_request.title)
- end
-
- it 'returns an array of open merge_requests' do
- path = endpoint_path + '?state=opened'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.last['title']).to eq(merge_request.title)
- end
-
- it 'returns an array of closed merge_requests' do
- path = endpoint_path + '?state=closed'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['title']).to eq(merge_request_closed.title)
- end
-
- it 'returns an array of merged merge_requests' do
- path = endpoint_path + '?state=merged'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['title']).to eq(merge_request_merged.title)
- end
-
- it 'matches V4 response schema' do
- get api(endpoint_path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to match_response_schema('public_api/v4/merge_requests')
- end
-
- it 'returns an empty array if no issue matches milestone' do
- get api(endpoint_path, user), params: { milestone: '1.0.0' }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if milestone does not exist' do
- get api(endpoint_path, user), params: { milestone: 'foo' }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an array of merge requests in given milestone' do
- get api(endpoint_path, user), params: { milestone: '0.9' }
-
- closed_issues = json_response.select { |mr| mr['id'] == merge_request_closed.id }
- expect(closed_issues.length).to eq(1)
- expect(closed_issues.first['title']).to eq merge_request_closed.title
- end
-
- it 'returns an array of merge requests matching state in milestone' do
- get api(endpoint_path, user), params: { milestone: '0.9', state: 'closed' }
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request_closed.id)
- end
-
- it 'returns an array of labeled merge requests' do
- path = endpoint_path + "?labels=#{label.title}"
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['labels']).to eq([label2.title, label.title])
- end
-
- it 'returns an array of labeled merge requests where all labels match' do
- path = endpoint_path + "?labels=#{label.title},foo,bar"
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an empty array if no merge request matches labels' do
- path = endpoint_path + '?labels=foo,bar'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(0)
- end
-
- it 'returns an array of merge requests with any label when filtering by any label' do
- get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_ANY }
-
- expect_paginated_array_response
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(merge_request.id)
- end
-
- it 'returns an array of merge requests without a label when filtering by no label' do
- get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_NONE }
-
- response_ids = json_response.map { |merge_request| merge_request['id'] }
-
- expect_paginated_array_response
- expect(response_ids).to contain_exactly(merge_request_closed.id, merge_request_merged.id, merge_request_locked.id)
- end
-
- it 'returns an array of labeled merge requests that are merged for a milestone' do
- bug_label = create(:label, title: 'bug', color: '#FFAABB', project: project)
-
- mr1 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone)
- mr2 = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
- mr3 = create(:merge_request, state: 'closed', source_project: project, target_project: project, milestone: milestone1)
- _mr = create(:merge_request, state: 'merged', source_project: project, target_project: project, milestone: milestone1)
-
- create(:label_link, label: bug_label, target: mr1)
- create(:label_link, label: bug_label, target: mr2)
- create(:label_link, label: bug_label, target: mr3)
-
- path = endpoint_path + "?labels=#{bug_label.title}&milestone=#{milestone1.title}&state=merged"
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
- expect(json_response.first['id']).to eq(mr2.id)
- end
-
- context 'with ordering' do
- before do
- @mr_later = mr_with_later_created_and_updated_at_time
- @mr_earlier = mr_with_earlier_created_and_updated_at_time
- end
-
- it 'returns an array of merge_requests in ascending order' do
- path = endpoint_path + '?sort=asc'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort)
- end
-
- it 'returns an array of merge_requests in descending order' do
- path = endpoint_path + '?sort=desc'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
-
- context '2 merge requests with equal created_at' do
- let!(:closed_mr2) do
- create :merge_request,
- state: 'closed',
- milestone: milestone1,
- author: user,
- assignee: user,
- source_project: project,
- target_project: project,
- title: "Test",
- created_at: @mr_earlier.created_at
- end
-
- it 'page breaks first page correctly' do
- get api("#{endpoint_path}?sort=desc&per_page=4", user)
-
- response_ids = json_response.map { |merge_request| merge_request['id'] }
-
- expect(response_ids).to include(closed_mr2.id)
- expect(response_ids).not_to include(@mr_earlier.id)
- end
-
- it 'page breaks second page correctly' do
- get api("#{endpoint_path}?sort=desc&per_page=4&page=2", user)
-
- response_ids = json_response.map { |merge_request| merge_request['id'] }
-
- expect(response_ids).not_to include(closed_mr2.id)
- expect(response_ids).to include(@mr_earlier.id)
- end
- end
-
- it 'returns an array of merge_requests ordered by updated_at' do
- path = endpoint_path + '?order_by=updated_at'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
- expect(response_dates).to eq(response_dates.sort.reverse)
- end
-
- it 'returns an array of merge_requests ordered by created_at' do
- path = endpoint_path + '?order_by=created_at&sort=asc'
-
- get api(path, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.length).to eq(4)
- response_dates = json_response.map { |merge_request| merge_request['created_at'] }
- expect(response_dates).to eq(response_dates.sort)
- end
- end
-
- context 'source_branch param' do
- it 'returns merge requests with the given source branch' do
- get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' }
-
- expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
- end
- end
-
- context 'target_branch param' do
- it 'returns merge requests with the given target branch' do
- get api(endpoint_path, user), params: { target_branch: merge_request_closed.target_branch, state: 'all' }
-
- expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/services/base_helm_service_shared_examples.rb b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb
new file mode 100644
index 00000000000..fa76b95f768
--- /dev/null
+++ b/spec/support/shared_examples/services/base_helm_service_shared_examples.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+shared_examples 'logs kubernetes errors' do
+ let(:error_hash) do
+ {
+ service: service.class.name,
+ app_id: application.id,
+ project_ids: application.cluster.project_ids,
+ group_ids: [],
+ error_code: error_code
+ }
+ end
+
+ let(:logger_hash) do
+ error_hash.merge(
+ exception: error_name,
+ message: error_message,
+ backtrace: instance_of(Array)
+ )
+ end
+
+ it 'logs into kubernetes.log and Sentry' do
+ expect(service.send(:logger)).to receive(:error).with(hash_including(logger_hash))
+
+ expect(Gitlab::Sentry).to receive(:track_acceptable_exception).with(
+ error,
+ extra: hash_including(error_hash)
+ )
+
+ service.execute
+ end
+end
diff --git a/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb b/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb
index 14638a574a5..02de47a96dd 100644
--- a/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb
+++ b/spec/support/shared_examples/services/check_ingress_ip_address_service_shared_examples.rb
@@ -12,6 +12,14 @@ shared_examples 'check ingress ip executions' do |app_name|
end
end
+ context 'when the ingress external hostname is available' do
+ it 'updates the external_hostname for the app' do
+ subject
+
+ expect(application.external_hostname).to eq('localhost.localdomain')
+ end
+ end
+
context 'when the ingress ip address is not available' do
let(:ingress) { nil }
diff --git a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
index 940c24c8d67..36c486dbdd6 100644
--- a/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/shared_examples/slack_mattermost_notifications_shared_examples.rb
@@ -106,6 +106,14 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(WebMock).to have_requested(:post, webhook_url).once
end
+ it "calls Slack/Mattermost API for deployment events" do
+ deployment_event_data = { object_kind: 'deployment' }
+
+ chat_service.execute(deployment_event_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
it 'uses the username as an option for slack when configured' do
allow(chat_service).to receive(:username).and_return(username)
@@ -267,7 +275,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'does not notify push events if they are not for the default branch' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
@@ -284,7 +292,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'still notifies about pushed tags' do
ref = "#{Gitlab::Git::TAG_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
@@ -299,7 +307,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
it 'notifies about all push events' do
ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
- push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+ push_sample_data = Gitlab::DataBuilder::Push.build(project: project, user: user, ref: ref)
chat_service.execute(push_sample_data)
diff --git a/spec/support/shared_examples/snippet_visibility.rb b/spec/support/shared_examples/snippet_visibility.rb
deleted file mode 100644
index 3a7c69b7877..00000000000
--- a/spec/support/shared_examples/snippet_visibility.rb
+++ /dev/null
@@ -1,322 +0,0 @@
-RSpec.shared_examples 'snippet visibility' do
- let!(:author) { create(:user) }
- let!(:member) { create(:user) }
- let!(:external) { create(:user, :external) }
-
- let!(:snippet_type_visibilities) do
- {
- public: Snippet::PUBLIC,
- internal: Snippet::INTERNAL,
- private: Snippet::PRIVATE
- }
- end
-
- context "For project snippets" do
- let!(:users) do
- {
- unauthenticated: nil,
- external: external,
- non_member: create(:user),
- member: member,
- author: author
- }
- end
-
- let!(:project_type_visibilities) do
- {
- public: Gitlab::VisibilityLevel::PUBLIC,
- internal: Gitlab::VisibilityLevel::INTERNAL,
- private: Gitlab::VisibilityLevel::PRIVATE
- }
- end
-
- let(:project_feature_visibilities) do
- {
- enabled: ProjectFeature::ENABLED,
- private: ProjectFeature::PRIVATE,
- disabled: ProjectFeature::DISABLED
- }
- end
-
- where(:project_type, :feature_visibility, :user_type, :snippet_type, :outcome) do
- [
- # Public projects
- [:public, :enabled, :unauthenticated, :public, true],
- [:public, :enabled, :unauthenticated, :internal, false],
- [:public, :enabled, :unauthenticated, :private, false],
-
- [:public, :enabled, :external, :public, true],
- [:public, :enabled, :external, :internal, false],
- [:public, :enabled, :external, :private, false],
-
- [:public, :enabled, :non_member, :public, true],
- [:public, :enabled, :non_member, :internal, true],
- [:public, :enabled, :non_member, :private, false],
-
- [:public, :enabled, :member, :public, true],
- [:public, :enabled, :member, :internal, true],
- [:public, :enabled, :member, :private, true],
-
- [:public, :enabled, :author, :public, true],
- [:public, :enabled, :author, :internal, true],
- [:public, :enabled, :author, :private, true],
-
- [:public, :private, :unauthenticated, :public, false],
- [:public, :private, :unauthenticated, :internal, false],
- [:public, :private, :unauthenticated, :private, false],
-
- [:public, :private, :external, :public, false],
- [:public, :private, :external, :internal, false],
- [:public, :private, :external, :private, false],
-
- [:public, :private, :non_member, :public, false],
- [:public, :private, :non_member, :internal, false],
- [:public, :private, :non_member, :private, false],
-
- [:public, :private, :member, :public, true],
- [:public, :private, :member, :internal, true],
- [:public, :private, :member, :private, true],
-
- [:public, :private, :author, :public, true],
- [:public, :private, :author, :internal, true],
- [:public, :private, :author, :private, true],
-
- [:public, :disabled, :unauthenticated, :public, false],
- [:public, :disabled, :unauthenticated, :internal, false],
- [:public, :disabled, :unauthenticated, :private, false],
-
- [:public, :disabled, :external, :public, false],
- [:public, :disabled, :external, :internal, false],
- [:public, :disabled, :external, :private, false],
-
- [:public, :disabled, :non_member, :public, false],
- [:public, :disabled, :non_member, :internal, false],
- [:public, :disabled, :non_member, :private, false],
-
- [:public, :disabled, :member, :public, false],
- [:public, :disabled, :member, :internal, false],
- [:public, :disabled, :member, :private, false],
-
- [:public, :disabled, :author, :public, false],
- [:public, :disabled, :author, :internal, false],
- [:public, :disabled, :author, :private, false],
-
- # Internal projects
- [:internal, :enabled, :unauthenticated, :public, false],
- [:internal, :enabled, :unauthenticated, :internal, false],
- [:internal, :enabled, :unauthenticated, :private, false],
-
- [:internal, :enabled, :external, :public, false],
- [:internal, :enabled, :external, :internal, false],
- [:internal, :enabled, :external, :private, false],
-
- [:internal, :enabled, :non_member, :public, true],
- [:internal, :enabled, :non_member, :internal, true],
- [:internal, :enabled, :non_member, :private, false],
-
- [:internal, :enabled, :member, :public, true],
- [:internal, :enabled, :member, :internal, true],
- [:internal, :enabled, :member, :private, true],
-
- [:internal, :enabled, :author, :public, true],
- [:internal, :enabled, :author, :internal, true],
- [:internal, :enabled, :author, :private, true],
-
- [:internal, :private, :unauthenticated, :public, false],
- [:internal, :private, :unauthenticated, :internal, false],
- [:internal, :private, :unauthenticated, :private, false],
-
- [:internal, :private, :external, :public, false],
- [:internal, :private, :external, :internal, false],
- [:internal, :private, :external, :private, false],
-
- [:internal, :private, :non_member, :public, false],
- [:internal, :private, :non_member, :internal, false],
- [:internal, :private, :non_member, :private, false],
-
- [:internal, :private, :member, :public, true],
- [:internal, :private, :member, :internal, true],
- [:internal, :private, :member, :private, true],
-
- [:internal, :private, :author, :public, true],
- [:internal, :private, :author, :internal, true],
- [:internal, :private, :author, :private, true],
-
- [:internal, :disabled, :unauthenticated, :public, false],
- [:internal, :disabled, :unauthenticated, :internal, false],
- [:internal, :disabled, :unauthenticated, :private, false],
-
- [:internal, :disabled, :external, :public, false],
- [:internal, :disabled, :external, :internal, false],
- [:internal, :disabled, :external, :private, false],
-
- [:internal, :disabled, :non_member, :public, false],
- [:internal, :disabled, :non_member, :internal, false],
- [:internal, :disabled, :non_member, :private, false],
-
- [:internal, :disabled, :member, :public, false],
- [:internal, :disabled, :member, :internal, false],
- [:internal, :disabled, :member, :private, false],
-
- [:internal, :disabled, :author, :public, false],
- [:internal, :disabled, :author, :internal, false],
- [:internal, :disabled, :author, :private, false],
-
- # Private projects
- [:private, :enabled, :unauthenticated, :public, false],
- [:private, :enabled, :unauthenticated, :internal, false],
- [:private, :enabled, :unauthenticated, :private, false],
-
- [:private, :enabled, :external, :public, true],
- [:private, :enabled, :external, :internal, true],
- [:private, :enabled, :external, :private, true],
-
- [:private, :enabled, :non_member, :public, false],
- [:private, :enabled, :non_member, :internal, false],
- [:private, :enabled, :non_member, :private, false],
-
- [:private, :enabled, :member, :public, true],
- [:private, :enabled, :member, :internal, true],
- [:private, :enabled, :member, :private, true],
-
- [:private, :enabled, :author, :public, true],
- [:private, :enabled, :author, :internal, true],
- [:private, :enabled, :author, :private, true],
-
- [:private, :private, :unauthenticated, :public, false],
- [:private, :private, :unauthenticated, :internal, false],
- [:private, :private, :unauthenticated, :private, false],
-
- [:private, :private, :external, :public, true],
- [:private, :private, :external, :internal, true],
- [:private, :private, :external, :private, true],
-
- [:private, :private, :non_member, :public, false],
- [:private, :private, :non_member, :internal, false],
- [:private, :private, :non_member, :private, false],
-
- [:private, :private, :member, :public, true],
- [:private, :private, :member, :internal, true],
- [:private, :private, :member, :private, true],
-
- [:private, :private, :author, :public, true],
- [:private, :private, :author, :internal, true],
- [:private, :private, :author, :private, true],
-
- [:private, :disabled, :unauthenticated, :public, false],
- [:private, :disabled, :unauthenticated, :internal, false],
- [:private, :disabled, :unauthenticated, :private, false],
-
- [:private, :disabled, :external, :public, false],
- [:private, :disabled, :external, :internal, false],
- [:private, :disabled, :external, :private, false],
-
- [:private, :disabled, :non_member, :public, false],
- [:private, :disabled, :non_member, :internal, false],
- [:private, :disabled, :non_member, :private, false],
-
- [:private, :disabled, :member, :public, false],
- [:private, :disabled, :member, :internal, false],
- [:private, :disabled, :member, :private, false],
-
- [:private, :disabled, :author, :public, false],
- [:private, :disabled, :author, :internal, false],
- [:private, :disabled, :author, :private, false]
- ]
- end
-
- with_them do
- let!(:project) { create(:project, visibility_level: project_type_visibilities[project_type]) }
- let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, project_feature_visibilities[feature_visibility]) }
- let!(:user) { users[user_type] }
- let!(:snippet) { create(:project_snippet, visibility_level: snippet_type_visibilities[snippet_type], project: project, author: author) }
- let!(:members) do
- project.add_developer(author)
- project.add_developer(member)
- project.add_developer(external) if project.private?
- end
-
- context "For #{params[:project_type]} project and #{params[:user_type]} users" do
- it 'should agree with the read_project_snippet policy' do
- expect(can?(user, :read_project_snippet, snippet)).to eq(outcome)
- end
-
- it 'should return proper outcome' do
- results = described_class.new(user, project: project).execute
- expect(results.include?(snippet)).to eq(outcome)
- end
- end
-
- context "Without a given project and #{params[:user_type]} users" do
- it 'should return proper outcome' do
- results = described_class.new(user).execute
- expect(results.include?(snippet)).to eq(outcome)
- end
-
- it 'returns no snippets when the user cannot read cross project' do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
-
- snippets = described_class.new(user).execute
-
- expect(snippets).to be_empty
- end
- end
- end
- end
-
- context 'For personal snippets' do
- let!(:users) do
- {
- unauthenticated: nil,
- external: external,
- non_member: create(:user),
- author: author
- }
- end
-
- where(:snippet_visibility, :user_type, :outcome) do
- [
- [:public, :unauthenticated, true],
- [:public, :external, true],
- [:public, :non_member, true],
- [:public, :author, true],
-
- [:internal, :unauthenticated, false],
- [:internal, :external, false],
- [:internal, :non_member, true],
- [:internal, :author, true],
-
- [:private, :unauthenticated, false],
- [:private, :external, false],
- [:private, :non_member, false],
- [:private, :author, true]
- ]
- end
-
- with_them do
- let!(:user) { users[user_type] }
- let!(:snippet) { create(:personal_snippet, visibility_level: snippet_type_visibilities[snippet_visibility], author: author) }
-
- context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do
- it 'should agree with read_personal_snippet policy' do
- expect(can?(user, :read_personal_snippet, snippet)).to eq(outcome)
- end
-
- it 'should return proper outcome' do
- results = described_class.new(user).execute
- expect(results.include?(snippet)).to eq(outcome)
- end
-
- it 'should return personal snippets when the user cannot read cross project' do
- allow(Ability).to receive(:allowed?).and_call_original
- allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
-
- results = described_class.new(user).execute
-
- expect(results.include?(snippet)).to eq(outcome)
- end
- end
- end
- end
-end
diff --git a/spec/support/shared_examples/snippet_visibility_shared_examples.rb b/spec/support/shared_examples/snippet_visibility_shared_examples.rb
new file mode 100644
index 00000000000..833c31a57cb
--- /dev/null
+++ b/spec/support/shared_examples/snippet_visibility_shared_examples.rb
@@ -0,0 +1,306 @@
+RSpec.shared_examples 'snippet visibility' do
+ using RSpec::Parameterized::TableSyntax
+
+ # Make sure no snippets exist prior to running the test matrix
+ before(:context) do
+ DatabaseCleaner.clean_with(:truncation)
+ end
+
+ set(:author) { create(:user) }
+ set(:member) { create(:user) }
+ set(:external) { create(:user, :external) }
+
+ context "For project snippets" do
+ let!(:users) do
+ {
+ unauthenticated: nil,
+ external: external,
+ non_member: create(:user),
+ member: member,
+ author: author
+ }
+ end
+
+ where(:project_type, :feature_visibility, :user_type, :snippet_type, :outcome) do
+ [
+ # Public projects
+ [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, true],
+ [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, true],
+ [:public, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, true],
+ [:public, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, true],
+ [:public, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true],
+ [:public, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true],
+ [:public, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true],
+
+ [:public, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true],
+ [:public, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true],
+ [:public, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true],
+
+ [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false],
+ [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, false],
+ [:public, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false],
+ [:public, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true],
+ [:public, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true],
+ [:public, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true],
+
+ [:public, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true],
+ [:public, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true],
+ [:public, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true],
+
+ [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false],
+ [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false],
+ [:public, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false],
+ [:public, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false],
+ [:public, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false],
+
+ [:public, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false],
+ [:public, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false],
+ [:public, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false],
+
+ # Internal projects
+ [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, true],
+ [:internal, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, true],
+ [:internal, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true],
+ [:internal, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true],
+ [:internal, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true],
+
+ [:internal, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true],
+ [:internal, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true],
+ [:internal, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true],
+
+ [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true],
+ [:internal, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true],
+ [:internal, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true],
+
+ [:internal, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true],
+ [:internal, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true],
+ [:internal, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true],
+
+ [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false],
+
+ [:internal, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false],
+ [:internal, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false],
+ [:internal, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false],
+
+ # Private projects
+ [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::ENABLED, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:private, ProjectFeature::ENABLED, :external, Snippet::PUBLIC, true],
+ [:private, ProjectFeature::ENABLED, :external, Snippet::INTERNAL, true],
+ [:private, ProjectFeature::ENABLED, :external, Snippet::PRIVATE, true],
+
+ [:private, ProjectFeature::ENABLED, :non_member, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::ENABLED, :non_member, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::ENABLED, :non_member, Snippet::PRIVATE, false],
+
+ [:private, ProjectFeature::ENABLED, :member, Snippet::PUBLIC, true],
+ [:private, ProjectFeature::ENABLED, :member, Snippet::INTERNAL, true],
+ [:private, ProjectFeature::ENABLED, :member, Snippet::PRIVATE, true],
+
+ [:private, ProjectFeature::ENABLED, :author, Snippet::PUBLIC, true],
+ [:private, ProjectFeature::ENABLED, :author, Snippet::INTERNAL, true],
+ [:private, ProjectFeature::ENABLED, :author, Snippet::PRIVATE, true],
+
+ [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::PRIVATE, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:private, ProjectFeature::PRIVATE, :external, Snippet::PUBLIC, true],
+ [:private, ProjectFeature::PRIVATE, :external, Snippet::INTERNAL, true],
+ [:private, ProjectFeature::PRIVATE, :external, Snippet::PRIVATE, true],
+
+ [:private, ProjectFeature::PRIVATE, :non_member, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::PRIVATE, :non_member, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::PRIVATE, :non_member, Snippet::PRIVATE, false],
+
+ [:private, ProjectFeature::PRIVATE, :member, Snippet::PUBLIC, true],
+ [:private, ProjectFeature::PRIVATE, :member, Snippet::INTERNAL, true],
+ [:private, ProjectFeature::PRIVATE, :member, Snippet::PRIVATE, true],
+
+ [:private, ProjectFeature::PRIVATE, :author, Snippet::PUBLIC, true],
+ [:private, ProjectFeature::PRIVATE, :author, Snippet::INTERNAL, true],
+ [:private, ProjectFeature::PRIVATE, :author, Snippet::PRIVATE, true],
+
+ [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::DISABLED, :unauthenticated, Snippet::PRIVATE, false],
+
+ [:private, ProjectFeature::DISABLED, :external, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::DISABLED, :external, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::DISABLED, :external, Snippet::PRIVATE, false],
+
+ [:private, ProjectFeature::DISABLED, :non_member, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::DISABLED, :non_member, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::DISABLED, :non_member, Snippet::PRIVATE, false],
+
+ [:private, ProjectFeature::DISABLED, :member, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::DISABLED, :member, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::DISABLED, :member, Snippet::PRIVATE, false],
+
+ [:private, ProjectFeature::DISABLED, :author, Snippet::PUBLIC, false],
+ [:private, ProjectFeature::DISABLED, :author, Snippet::INTERNAL, false],
+ [:private, ProjectFeature::DISABLED, :author, Snippet::PRIVATE, false]
+ ]
+ end
+
+ with_them do
+ let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel.level_value(project_type.to_s)) }
+ let!(:project_feature) { project.project_feature.update_column(:snippets_access_level, feature_visibility) }
+ let!(:user) { users[user_type] }
+ let!(:snippet) { create(:project_snippet, visibility_level: snippet_type, project: project, author: author) }
+ let!(:members) do
+ project.add_developer(author)
+ project.add_developer(member)
+ project.add_developer(external) if project.private?
+ end
+
+ context "For #{params[:project_type]} project and #{params[:user_type]} users" do
+ it 'agrees with the read_project_snippet policy' do
+ expect(can?(user, :read_project_snippet, snippet)).to eq(outcome)
+ end
+
+ it 'returns proper outcome' do
+ results = described_class.new(user, project: project).execute
+
+ expect(results.include?(snippet)).to eq(outcome)
+ end
+ end
+
+ context "Without a given project and #{params[:user_type]} users" do
+ it 'returns proper outcome' do
+ results = described_class.new(user).execute
+ expect(results.include?(snippet)).to eq(outcome)
+ end
+
+ it 'returns no snippets when the user cannot read cross project' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+
+ snippets = described_class.new(user).execute
+
+ expect(snippets).to be_empty
+ end
+ end
+ end
+ end
+
+ context 'For personal snippets' do
+ let!(:users) do
+ {
+ unauthenticated: nil,
+ external: external,
+ non_member: create(:user),
+ author: author
+ }
+ end
+
+ where(:snippet_visibility, :user_type, :outcome) do
+ [
+ [Snippet::PUBLIC, :unauthenticated, true],
+ [Snippet::PUBLIC, :external, true],
+ [Snippet::PUBLIC, :non_member, true],
+ [Snippet::PUBLIC, :author, true],
+
+ [Snippet::INTERNAL, :unauthenticated, false],
+ [Snippet::INTERNAL, :external, false],
+ [Snippet::INTERNAL, :non_member, true],
+ [Snippet::INTERNAL, :author, true],
+
+ [Snippet::PRIVATE, :unauthenticated, false],
+ [Snippet::PRIVATE, :external, false],
+ [Snippet::PRIVATE, :non_member, false],
+ [Snippet::PRIVATE, :author, true]
+ ]
+ end
+
+ with_them do
+ let!(:user) { users[user_type] }
+ let!(:snippet) { create(:personal_snippet, visibility_level: snippet_visibility, author: author) }
+
+ context "For personal and #{params[:snippet_visibility]} snippets with #{params[:user_type]} user" do
+ it 'agrees with read_personal_snippet policy' do
+ expect(can?(user, :read_personal_snippet, snippet)).to eq(outcome)
+ end
+
+ it 'returns proper outcome' do
+ results = described_class.new(user).execute
+ expect(results.include?(snippet)).to eq(outcome)
+ end
+
+ it 'returns personal snippets when the user cannot read cross project' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false }
+
+ results = described_class.new(user).execute
+
+ expect(results.include?(snippet)).to eq(outcome)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/url_validator_examples.rb b/spec/support/shared_examples/url_validator_examples.rb
index 1f7e2f7ff79..25277ccd9aa 100644
--- a/spec/support/shared_examples/url_validator_examples.rb
+++ b/spec/support/shared_examples/url_validator_examples.rb
@@ -1,15 +1,15 @@
-RSpec.shared_examples 'url validator examples' do |protocols|
+RSpec.shared_examples 'url validator examples' do |schemes|
let(:validator) { described_class.new(attributes: [:link_url], **options) }
let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
- subject { validator.validate_each(badge, :link_url, badge.link_url) }
+ subject { validator.validate(badge) }
- describe '#validates_each' do
+ describe '#validate' do
context 'with no options' do
let(:options) { {} }
- it "allows #{protocols.join(',')} protocols by default" do
- expect(validator.send(:default_options)[:protocols]).to eq protocols
+ it "allows #{schemes.join(',')} schemes by default" do
+ expect(validator.options[:schemes]).to eq schemes
end
it 'checks that the url structure is valid' do
@@ -17,25 +17,25 @@ RSpec.shared_examples 'url validator examples' do |protocols|
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
end
- context 'with protocols' do
- let(:options) { { protocols: %w[http] } }
+ context 'with schemes' do
+ let(:options) { { schemes: %w(http) } }
- it 'allows urls with the defined protocols' do
+ it 'allows urls with the defined schemes' do
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors).to be_empty
end
- it 'add error if the url protocol does not match the selected ones' do
+ it 'add error if the url scheme does not match the selected ones' do
badge.link_url = 'https://www.example.com'
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
end
end
diff --git a/spec/support/shared_examples/views/nav_sidebar.rb b/spec/support/shared_examples/views/nav_sidebar.rb
new file mode 100644
index 00000000000..6ac5abe275d
--- /dev/null
+++ b/spec/support/shared_examples/views/nav_sidebar.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+shared_examples 'has nav sidebar' do
+ it 'has collapsed nav sidebar on mobile' do
+ render
+
+ expect(rendered).to have_selector('.nav-sidebar')
+ expect(rendered).not_to have_selector('.sidebar-collapsed-desktop')
+ expect(rendered).not_to have_selector('.sidebar-expanded-mobile')
+ end
+end
diff --git a/spec/support/shared_examples/wiki_file_attachments_examples.rb b/spec/support/shared_examples/wiki_file_attachments_examples.rb
index b6fb2a66b0e..22fbfb48928 100644
--- a/spec/support/shared_examples/wiki_file_attachments_examples.rb
+++ b/spec/support/shared_examples/wiki_file_attachments_examples.rb
@@ -42,7 +42,7 @@ shared_examples 'wiki file attachments' do
end
end
- context 'uploading is complete' do
+ context 'uploading is complete', :quarantine do
it 'shows "Attach a file" button on uploading complete' do
attach_with_dropzone
wait_for_requests
diff --git a/spec/support/shoulda/matchers/rails_shim.rb b/spec/support/shoulda/matchers/rails_shim.rb
deleted file mode 100644
index 8d70598beb5..00000000000
--- a/spec/support/shoulda/matchers/rails_shim.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# monkey patch which fixes serialization matcher in Rails 5
-# https://github.com/thoughtbot/shoulda-matchers/issues/913
-# This can be removed when a new version of shoulda-matchers
-# is released
-module Shoulda
- module Matchers
- class RailsShim
- def self.serialized_attributes_for(model)
- if defined?(::ActiveRecord::Type::Serialized)
- # Rails 5+
- serialized_columns = model.columns.select do |column|
- model.type_for_attribute(column.name).is_a?(
- ::ActiveRecord::Type::Serialized
- )
- end
-
- serialized_columns.inject({}) do |hash, column| # rubocop:disable Style/EachWithObject
- hash[column.name.to_s] = model.type_for_attribute(column.name).coder
- hash
- end
- else
- model.serialized_attributes
- end
- end
- end
- end
-end
diff --git a/spec/support/webmock.rb b/spec/support/webmock.rb
index af2906b7568..9ac7e7fc515 100644
--- a/spec/support/webmock.rb
+++ b/spec/support/webmock.rb
@@ -1,4 +1,12 @@
require 'webmock'
require 'webmock/rspec'
-WebMock.disable_net_connect!(allow_localhost: true)
+def webmock_allowed_hosts
+ %w[elasticsearch registry.gitlab.com-gitlab-org-test-elastic-image].tap do |hosts|
+ if ENV.key?('ELASTIC_URL')
+ hosts << URI.parse(ENV['ELASTIC_URL']).host
+ end
+ end.uniq
+end
+
+WebMock.disable_net_connect!(allow_localhost: true, allow: webmock_allowed_hosts)
diff --git a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
index 8544fb62b5a..be69c10d7c8 100644
--- a/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/artifacts/migrate_rake_spec.rb
@@ -13,61 +13,6 @@ describe 'gitlab:artifacts namespace rake task' do
subject { run_rake_task('gitlab:artifacts:migrate') }
- context 'legacy artifacts' do
- describe 'migrate' do
- let!(:build) { create(:ci_build, :legacy_artifacts, artifacts_file_store: store, artifacts_metadata_store: store) }
-
- context 'when local storage is used' do
- let(:store) { ObjectStorage::Store::LOCAL }
-
- context 'and job does not have file store defined' do
- let(:object_storage_enabled) { true }
- let(:store) { nil }
-
- it "migrates file to remote storage" do
- subject
-
- expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
-
- context 'and remote storage is defined' do
- let(:object_storage_enabled) { true }
-
- it "migrates file to remote storage" do
- subject
-
- expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
-
- context 'and remote storage is not defined' do
- it "fails to migrate to remote storage" do
- subject
-
- expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::LOCAL)
- expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::LOCAL)
- end
- end
- end
-
- context 'when remote storage is used' do
- let(:object_storage_enabled) { true }
-
- let(:store) { ObjectStorage::Store::REMOTE }
-
- it "file stays on remote storage" do
- subject
-
- expect(build.reload.artifacts_file_store).to eq(ObjectStorage::Store::REMOTE)
- expect(build.reload.artifacts_metadata_store).to eq(ObjectStorage::Store::REMOTE)
- end
- end
- end
- end
-
context 'job artifacts' do
let!(:artifact) { create(:ci_job_artifact, :archive, file_store: store) }
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index a8fae4a88a3..bdbd39475b9 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -21,9 +21,6 @@ describe 'gitlab:app namespace rake task' do
# empty task as env is already loaded
Rake::Task.define_task :environment
-
- # We need this directory to run `gitlab:backup:create` task
- FileUtils.mkdir_p('public/uploads')
end
before do
@@ -38,6 +35,7 @@ describe 'gitlab:app namespace rake task' do
end
def run_rake_task(task_name)
+ FileUtils.mkdir_p('tmp/tests/public/uploads')
Rake::Task[task_name].reenable
Rake.application.invoke_task task_name
end
diff --git a/spec/tasks/gitlab/shell_rake_spec.rb b/spec/tasks/gitlab/shell_rake_spec.rb
index 0ed5d3e27b9..c3e912b02c5 100644
--- a/spec/tasks/gitlab/shell_rake_spec.rb
+++ b/spec/tasks/gitlab/shell_rake_spec.rb
@@ -7,14 +7,8 @@ describe 'gitlab:shell rake tasks' do
stub_warn_user_is_not_gitlab
end
- after do
- TestEnv.create_fake_git_hooks
- end
-
describe 'install task' do
- it 'invokes create_hooks task' do
- expect(Rake::Task['gitlab:shell:create_hooks']).to receive(:invoke)
-
+ it 'installs and compiles gitlab-shell' do
storages = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
Gitlab.config.repositories.storages.values.map(&:legacy_disk_path)
end
@@ -24,14 +18,4 @@ describe 'gitlab:shell rake tasks' do
run_rake_task('gitlab:shell:install')
end
end
-
- describe 'create_hooks task' do
- it 'calls gitlab-shell bin/create_hooks' do
- expect_any_instance_of(Object).to receive(:system)
- .with("#{Gitlab.config.gitlab_shell.path}/bin/create-hooks",
- *Gitlab::TaskHelpers.repository_storage_paths_args)
-
- run_rake_task('gitlab:shell:create_hooks')
- end
- end
end
diff --git a/spec/tasks/gitlab/storage_rake_spec.rb b/spec/tasks/gitlab/storage_rake_spec.rb
index 6b50670c3c0..4b04d9cec39 100644
--- a/spec/tasks/gitlab/storage_rake_spec.rb
+++ b/spec/tasks/gitlab/storage_rake_spec.rb
@@ -1,6 +1,6 @@
require 'rake_helper'
-describe 'rake gitlab:storage:*' do
+describe 'rake gitlab:storage:*', :sidekiq do
before do
Rake.application.rake_require 'tasks/gitlab/storage'
@@ -43,9 +43,7 @@ describe 'rake gitlab:storage:*' do
end
end
- describe 'gitlab:storage:migrate_to_hashed' do
- let(:task) { 'gitlab:storage:migrate_to_hashed' }
-
+ shared_examples "make sure database is writable" do
context 'read-only database' do
it 'does nothing' do
expect(Gitlab::Database).to receive(:read_only?).and_return(true)
@@ -55,48 +53,68 @@ describe 'rake gitlab:storage:*' do
expect { run_rake_task(task) }.to output(/This task requires database write access. Exiting./).to_stderr
end
end
+ end
- context '0 legacy projects' do
- it 'does nothing' do
- expect(::HashedStorage::MigratorWorker).not_to receive(:perform_async)
+ shared_examples "handles custom BATCH env var" do |worker_klass|
+ context 'in batches of 1' do
+ before do
+ stub_env('BATCH' => 1)
+ end
+
+ it "enqueues one #{worker_klass} per project" do
+ projects.each do |project|
+ expect(worker_klass).to receive(:perform_async).with(project.id, project.id)
+ end
run_rake_task(task)
end
end
- context '3 legacy projects' do
- let(:projects) { create_list(:project, 3, :legacy_storage) }
+ context 'in batches of 2' do
+ before do
+ stub_env('BATCH' => 2)
+ end
- context 'in batches of 1' do
- before do
- stub_env('BATCH' => 1)
+ it "enqueues one #{worker_klass} per 2 projects" do
+ projects.map(&:id).sort.each_slice(2) do |first, last|
+ last ||= first
+ expect(worker_klass).to receive(:perform_async).with(first, last)
end
- it 'enqueues one HashedStorage::MigratorWorker per project' do
- projects.each do |project|
- expect(::HashedStorage::MigratorWorker).to receive(:perform_async).with(project.id, project.id)
- end
-
- run_rake_task(task)
- end
+ run_rake_task(task)
end
+ end
+ end
- context 'in batches of 2' do
- before do
- stub_env('BATCH' => 2)
- end
+ describe 'gitlab:storage:migrate_to_hashed' do
+ let(:task) { 'gitlab:storage:migrate_to_hashed' }
- it 'enqueues one HashedStorage::MigratorWorker per 2 projects' do
- projects.map(&:id).sort.each_slice(2) do |first, last|
- last ||= first
- expect(::HashedStorage::MigratorWorker).to receive(:perform_async).with(first, last)
- end
+ context 'with rollback already scheduled', :redis do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ ::HashedStorage::RollbackerWorker.perform_async(1, 5)
+
+ expect(Project).not_to receive(:with_unmigrated_storage)
- run_rake_task(task)
+ expect { run_rake_task(task) }.to output(/There is already a rollback operation in progress/).to_stderr
end
end
end
+ context 'with 0 legacy projects' do
+ it 'does nothing' do
+ expect(::HashedStorage::MigratorWorker).not_to receive(:perform_async)
+
+ run_rake_task(task)
+ end
+ end
+
+ context 'with 3 legacy projects' do
+ let(:projects) { create_list(:project, 3, :legacy_storage) }
+
+ it_behaves_like "handles custom BATCH env var", ::HashedStorage::MigratorWorker
+ end
+
context 'with same id in range' do
it 'displays message when project cant be found' do
stub_env('ID_FROM', 99999)
@@ -123,6 +141,38 @@ describe 'rake gitlab:storage:*' do
end
end
+ describe 'gitlab:storage:rollback_to_legacy' do
+ let(:task) { 'gitlab:storage:rollback_to_legacy' }
+
+ it_behaves_like 'make sure database is writable'
+
+ context 'with migration already scheduled', :redis do
+ it 'does nothing' do
+ Sidekiq::Testing.disable! do
+ ::HashedStorage::MigratorWorker.perform_async(1, 5)
+
+ expect(Project).not_to receive(:with_unmigrated_storage)
+
+ expect { run_rake_task(task) }.to output(/There is already a migration operation in progress/).to_stderr
+ end
+ end
+ end
+
+ context 'with 0 hashed projects' do
+ it 'does nothing' do
+ expect(::HashedStorage::RollbackerWorker).not_to receive(:perform_async)
+
+ run_rake_task(task)
+ end
+ end
+
+ context 'with 3 hashed projects' do
+ let(:projects) { create_list(:project, 3) }
+
+ it_behaves_like "handles custom BATCH env var", ::HashedStorage::RollbackerWorker
+ end
+ end
+
describe 'gitlab:storage:legacy_projects' do
it_behaves_like 'rake entities summary', 'projects', 'Legacy' do
let(:task) { 'gitlab:storage:legacy_projects' }
diff --git a/spec/tasks/tokens_spec.rb b/spec/tasks/tokens_spec.rb
index 555a58e9aa1..4188e7caccb 100644
--- a/spec/tasks/tokens_spec.rb
+++ b/spec/tasks/tokens_spec.rb
@@ -8,13 +8,13 @@ describe 'tokens rake tasks' do
end
describe 'reset_all_email task' do
- it 'invokes create_hooks task' do
+ it 'changes the incoming email token' do
expect { run_rake_task('tokens:reset_all_email') }.to change { user.reload.incoming_email_token }
end
end
describe 'reset_all_feed task' do
- it 'invokes create_hooks task' do
+ it 'changes the feed token for the user' do
expect { run_rake_task('tokens:reset_all_feed') }.to change { user.reload.feed_token }
end
end
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index de29d0c943f..e474a714b10 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe FileMover do
+ include FileMoverHelpers
+
let(:filename) { 'banana_sample.gif' }
- let(:file) { fixture_file_upload(File.join('spec', 'fixtures', filename)) }
let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) }
let(:temp_description) do
@@ -12,7 +13,7 @@ describe FileMover do
let(:file_path) { File.join('uploads/-/system/personal_snippet', snippet.id.to_s, 'secret55', filename) }
let(:snippet) { create(:personal_snippet, description: temp_description) }
- subject { described_class.new(file_path, snippet).execute }
+ subject { described_class.new(temp_file_path, snippet).execute }
describe '#execute' do
before do
@@ -20,6 +21,8 @@ describe FileMover do
expect(FileUtils).to receive(:move).with(a_string_including(temp_file_path), a_string_including(file_path))
allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:exists?).and_return(true)
allow_any_instance_of(CarrierWave::SanitizedFile).to receive(:size).and_return(10)
+
+ stub_file_mover(temp_file_path)
end
context 'when move and field update successful' do
@@ -66,4 +69,30 @@ describe FileMover do
end
end
end
+
+ context 'security' do
+ context 'when relative path is involved' do
+ let(:temp_file_path) { File.join('uploads/-/system/temp', '..', 'another_subdir_of_temp') }
+
+ it 'does not trigger move if path is outside designated directory' do
+ stub_file_mover('uploads/-/system/another_subdir_of_temp')
+ expect(FileUtils).not_to receive(:move)
+
+ subject
+
+ expect(snippet.reload.description).to eq(temp_description)
+ end
+ end
+
+ context 'when symlink is involved' do
+ it 'does not trigger move if path is outside designated directory' do
+ stub_file_mover(temp_file_path, stub_real_path: Pathname('/etc'))
+ expect(FileUtils).not_to receive(:move)
+
+ subject
+
+ expect(snippet.reload.description).to eq(temp_description)
+ end
+ end
+ end
end
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
index 825c1cabc14..2dea48e3a88 100644
--- a/spec/uploaders/import_export_uploader_spec.rb
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -3,9 +3,18 @@ require 'spec_helper'
describe ImportExportUploader do
let(:model) { build_stubbed(:import_export_upload) }
let(:upload) { create(:upload, model: model) }
+ let(:import_export_upload) { ImportExportUpload.new }
subject { described_class.new(model, :import_file) }
+ context 'local store' do
+ describe '#move_to_store' do
+ it 'returns true' do
+ expect(subject.move_to_store).to be true
+ end
+ end
+ end
+
context "object_store is REMOTE" do
before do
stub_uploads_object_storage
@@ -16,5 +25,28 @@ describe ImportExportUploader do
it_behaves_like 'builds correct paths',
store_dir: %r[import_export_upload/import_file/],
upload_path: %r[import_export_upload/import_file/]
+
+ describe '#move_to_store' do
+ it 'returns false' do
+ expect(subject.move_to_store).to be false
+ end
+ end
+
+ describe 'with an export file directly uploaded' do
+ let(:tempfile) { Tempfile.new(['test', '.gz']) }
+
+ before do
+ stub_uploads_object_storage(described_class, direct_upload: true)
+ import_export_upload.export_file = tempfile
+ end
+
+ it 'cleans up cached file' do
+ cache_dir = File.join(import_export_upload.export_file.cache_path(nil), '*')
+
+ import_export_upload.save!
+
+ expect(Dir[cache_dir]).to be_empty
+ end
+ end
end
end
diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb
deleted file mode 100644
index 0589563b502..00000000000
--- a/spec/uploaders/legacy_artifact_uploader_spec.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-require 'rails_helper'
-
-describe LegacyArtifactUploader do
- let(:store) { described_class::Store::LOCAL }
- let(:job) { create(:ci_build, artifacts_file_store: store) }
- let(:uploader) { described_class.new(job, :legacy_artifacts_file) }
- let(:local_path) { described_class.root }
-
- subject { uploader }
-
- # TODO: move to Workhorse::UploadPath
- describe '.workhorse_upload_path' do
- subject { described_class.workhorse_upload_path }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('tmp/uploads') }
- end
-
- it_behaves_like "builds correct paths",
- store_dir: %r[\d{4}_\d{1,2}/\d+/\d+\z],
- cache_dir: %r[artifacts/tmp/cache],
- work_dir: %r[artifacts/tmp/work]
-
- context 'object store is remote' do
- before do
- stub_artifacts_object_storage
- end
-
- include_context 'with storage', described_class::Store::REMOTE
-
- it_behaves_like "builds correct paths",
- store_dir: %r[\d{4}_\d{1,2}/\d+/\d+\z]
- end
-
- describe '#filename' do
- # we need to use uploader, as this makes to use mounter
- # which initialises uploader.file object
- let(:uploader) { job.artifacts_file }
-
- subject { uploader.filename }
-
- it { is_expected.to be_nil }
- end
-
- context 'file is stored in valid path' do
- let(:file) do
- fixture_file_upload('spec/fixtures/ci_build_artifacts.zip', 'application/zip')
- end
-
- before do
- uploader.store!(file)
- end
-
- subject { uploader.file.path }
-
- it { is_expected.to start_with("#{uploader.root}") }
- it { is_expected.to include("/#{job.created_at.utc.strftime('%Y_%m')}/") }
- it { is_expected.to include("/#{job.project_id}/") }
- it { is_expected.to end_with("ci_build_artifacts.zip") }
- end
-end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 533e9d87ea6..6bad5d49b1c 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -12,7 +12,7 @@ class Implementation < GitlabUploader
# user/:id
def dynamic_segment
- File.join(model.class.to_s.underscore, model.id.to_s)
+ File.join(model.class.underscore, model.id.to_s)
end
end
@@ -375,7 +375,7 @@ describe ObjectStorage do
describe '#fog_public' do
subject { uploader.fog_public }
- it { is_expected.to eq(false) }
+ it { is_expected.to eq(nil) }
end
describe '.workhorse_authorize' do
@@ -771,6 +771,14 @@ describe ObjectStorage do
expect { avatars }.not_to exceed_query_limit(1)
end
+ it 'does not attempt to replace methods' do
+ models.each do |model|
+ expect(model.avatar.upload).to receive(:method_missing).and_call_original
+
+ model.avatar.upload.path
+ end
+ end
+
it 'fetches a unique upload for each model' do
expect(avatars.map(&:url).uniq).to eq(avatars.map(&:url))
expect(avatars.map(&:upload).uniq).to eq(avatars.map(&:upload))
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index 97758f0243e..d9f0e2f3cb7 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -7,33 +7,19 @@ describe PersonalFileUploader do
subject { uploader }
- it_behaves_like 'builds correct paths',
- store_dir: %r[uploads/-/system/personal_snippet/\d+],
- upload_path: %r[\h+/\S+],
- absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet\/\d+\/\h+\/\S+$]
-
- context "object_store is REMOTE" do
+ shared_examples '#base_dir' do
before do
- stub_uploads_object_storage
+ subject.instance_variable_set(:@secret, 'secret')
end
- include_context 'with storage', described_class::Store::REMOTE
-
- it_behaves_like 'builds correct paths',
- store_dir: %r[\d+/\h+],
- upload_path: %r[^personal_snippet\/\d+\/\h+\/<filename>]
- end
-
- describe '#upload_paths' do
- it 'builds correct paths for both local and remote storage' do
- paths = uploader.upload_paths('test.jpg')
+ it 'is prefixed with uploads/-/system' do
+ allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
- expect(paths.first).to match(%r[\h+\/test.jpg])
- expect(paths.second).to match(%r[^personal_snippet\/\d+\/\h+\/test.jpg])
+ expect(described_class.base_dir(model)).to eq("uploads/-/system/personal_snippet/#{model.id}")
end
end
- describe '#to_h' do
+ shared_examples '#to_h' do
before do
subject.instance_variable_set(:@secret, 'secret')
end
@@ -50,6 +36,40 @@ describe PersonalFileUploader do
end
end
+ describe '#upload_paths' do
+ it 'builds correct paths for both local and remote storage' do
+ paths = uploader.upload_paths('test.jpg')
+
+ expect(paths.first).to match(%r[\h+\/test.jpg])
+ expect(paths.second).to match(%r[^personal_snippet\/\d+\/\h+\/test.jpg])
+ end
+ end
+
+ context 'object_store is LOCAL' do
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/personal_snippet/\d+/\h+],
+ upload_path: %r[\h+/\S+],
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet\/\d+\/\h+\/\S+$]
+
+ it_behaves_like '#base_dir'
+ it_behaves_like '#to_h'
+ end
+
+ context "object_store is REMOTE" do
+ before do
+ stub_uploads_object_storage
+ end
+
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[\d+/\h+],
+ upload_path: %r[^personal_snippet\/\d+\/\h+\/<filename>]
+
+ it_behaves_like '#base_dir'
+ it_behaves_like '#to_h'
+ end
+
describe "#migrate!" do
before do
uploader.store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 3592a11360d..42352f9b9f8 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -71,7 +71,7 @@ describe RecordsUploads do
expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count }
end
- it 'it destroys Upload records at the same path before recording' do
+ it 'destroys Upload records at the same path before recording' do
existing = Upload.create!(
path: File.join('uploads', 'rails_sample.jpg'),
size: 512.kilobytes,
@@ -88,10 +88,19 @@ describe RecordsUploads do
end
describe '#destroy_upload callback' do
- it 'it destroys Upload records at the same path after removal' do
+ it 'destroys Upload records at the same path after removal' do
uploader.store!(upload_fixture('rails_sample.jpg'))
expect { uploader.remove! }.to change { Upload.count }.from(1).to(0)
end
end
+
+ describe '#filename' do
+ it 'gets the filename from the path recorded in the database, not CarrierWave' do
+ uploader.store!(upload_fixture('rails_sample.jpg'))
+ expect_any_instance_of(GitlabUploader).not_to receive(:filename)
+
+ expect(uploader.filename).to eq('rails_sample.jpg')
+ end
+ end
end
diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
index 95813d15e52..cc8970d2ba0 100644
--- a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
+++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
@@ -48,40 +48,6 @@ describe ObjectStorage::BackgroundMoveWorker do
end
end
- context 'for legacy artifacts' do
- let(:build) { create(:ci_build, :legacy_artifacts) }
- let(:uploader_class) { LegacyArtifactUploader }
- let(:subject_class) { Ci::Build }
- let(:file_field) { :artifacts_file }
- let(:subject_id) { build.id }
-
- context 'when local storage is used' do
- let(:store) { local }
-
- context 'and remote storage is defined' do
- before do
- stub_artifacts_object_storage(background_upload: true)
- end
-
- it "migrates file to remote storage" do
- perform
-
- expect(build.reload.artifacts_file_store).to eq(remote)
- end
-
- context 'for artifacts_metadata' do
- let(:file_field) { :artifacts_metadata }
-
- it 'migrates metadata to remote storage' do
- perform
-
- expect(build.reload.artifacts_metadata_store).to eq(remote)
- end
- end
- end
- end
- end
-
context 'for job artifacts' do
let(:artifact) { create(:ci_job_artifact, :archive) }
let(:uploader_class) { JobArtifactUploader }
diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/addressable_url_validator_spec.rb
index 1bb42382e8a..387e84b2d04 100644
--- a/spec/validators/url_validator_spec.rb
+++ b/spec/validators/addressable_url_validator_spec.rb
@@ -2,11 +2,11 @@
require 'spec_helper'
-describe UrlValidator do
+describe AddressableUrlValidator do
let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
- subject { validator.validate_each(badge, :link_url, badge.link_url) }
+ subject { validator.validate(badge) }
- include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS
+ include_examples 'url validator examples', described_class::DEFAULT_OPTIONS[:schemes]
describe 'validations' do
include_context 'invalid urls'
@@ -14,13 +14,13 @@ describe UrlValidator do
let(:validator) { described_class.new(attributes: [:link_url]) }
it 'returns error when url is nil' do
- expect(validator.validate_each(badge, :link_url, nil)).to be_nil
- expect(badge.errors.first[1]).to eq 'must be a valid URL'
+ expect(validator.validate_each(badge, :link_url, nil)).to be_falsey
+ expect(badge.errors.first[1]).to eq validator.options.fetch(:message)
end
it 'returns error when url is empty' do
- expect(validator.validate_each(badge, :link_url, '')).to be_nil
- expect(badge.errors.first[1]).to eq 'must be a valid URL'
+ expect(validator.validate_each(badge, :link_url, '')).to be_falsey
+ expect(badge.errors.first[1]).to eq validator.options.fetch(:message)
end
it 'does not allow urls with CR or LF characters' do
@@ -30,6 +30,17 @@ describe UrlValidator do
end
end
end
+
+ it 'provides all arguments to UrlBlock validate' do
+ expect(Gitlab::UrlBlocker)
+ .to receive(:validate!)
+ .with(badge.link_url, described_class::BLOCKER_VALIDATE_OPTIONS)
+ .and_return(true)
+
+ subject
+
+ expect(badge.errors).to be_empty
+ end
end
context 'by default' do
@@ -40,7 +51,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors).to be_empty
end
it 'does not block urls pointing to the local network' do
@@ -48,7 +59,23 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors).to be_empty
+ end
+
+ it 'does block nil urls' do
+ badge.link_url = nil
+
+ subject
+
+ expect(badge.errors).to be_present
+ end
+
+ it 'does block blank urls' do
+ badge.link_url = '\n\r \n'
+
+ subject
+
+ expect(badge.errors).to be_present
end
it 'strips urls' do
@@ -67,6 +94,40 @@ describe UrlValidator do
end
end
+ context 'when message is set' do
+ let(:message) { 'is blocked: test message' }
+ let(:validator) { described_class.new(attributes: [:link_url], allow_nil: false, message: message) }
+
+ it 'does block nil url with provided error message' do
+ expect(validator.validate_each(badge, :link_url, nil)).to be_falsey
+ expect(badge.errors.first[1]).to eq message
+ end
+ end
+
+ context 'when allow_nil is set to true' do
+ let(:validator) { described_class.new(attributes: [:link_url], allow_nil: true) }
+
+ it 'does not block nil urls' do
+ badge.link_url = nil
+
+ subject
+
+ expect(badge.errors).to be_empty
+ end
+ end
+
+ context 'when allow_blank is set to true' do
+ let(:validator) { described_class.new(attributes: [:link_url], allow_blank: true) }
+
+ it 'does not block blank urls' do
+ badge.link_url = "\n\r \n"
+
+ subject
+
+ expect(badge.errors).to be_empty
+ end
+ end
+
context 'when allow_localhost is set to false' do
let(:validator) { described_class.new(attributes: [:link_url], allow_localhost: false) }
@@ -75,7 +136,21 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
+ end
+
+ context 'when allow_setting_local_requests is set to true' do
+ it 'does not block urls pointing to localhost' do
+ expect(described_class)
+ .to receive(:allow_setting_local_requests?)
+ .and_return(true)
+
+ badge.link_url = 'https://127.0.0.1'
+
+ subject
+
+ expect(badge.errors).to be_empty
+ end
end
end
@@ -87,7 +162,21 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
+ end
+
+ context 'when allow_setting_local_requests is set to true' do
+ it 'does not block urls pointing to local network' do
+ expect(described_class)
+ .to receive(:allow_setting_local_requests?)
+ .and_return(true)
+
+ badge.link_url = 'https://192.168.1.1'
+
+ subject
+
+ expect(badge.errors).to be_empty
+ end
end
end
@@ -100,7 +189,7 @@ describe UrlValidator do
it 'does not block any port' do
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors).to be_empty
end
end
@@ -110,7 +199,7 @@ describe UrlValidator do
it 'blocks urls with a different port' do
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
end
end
@@ -127,7 +216,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
end
@@ -139,7 +228,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors).to be_empty
end
end
end
@@ -156,7 +245,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
end
@@ -168,7 +257,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors).to be_empty
end
end
end
@@ -191,7 +280,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
it 'prevents unsafe internal urls' do
@@ -199,7 +288,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
it 'allows safe urls' do
@@ -207,7 +296,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors).to be_empty
end
end
@@ -219,7 +308,7 @@ describe UrlValidator do
subject
- expect(badge.errors.empty?).to be true
+ expect(badge.errors).to be_empty
end
end
end
diff --git a/spec/validators/devise_email_validator_spec.rb b/spec/validators/devise_email_validator_spec.rb
new file mode 100644
index 00000000000..7860b659bd3
--- /dev/null
+++ b/spec/validators/devise_email_validator_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DeviseEmailValidator do
+ let!(:user) { build(:user, public_email: 'test@example.com') }
+ subject { validator.validate(user) }
+
+ describe 'validations' do
+ context 'by default' do
+ let(:validator) { described_class.new(attributes: [:public_email]) }
+
+ it 'allows when email is valid' do
+ subject
+
+ expect(user.errors).to be_empty
+ end
+
+ it 'returns error when email is invalid' do
+ user.public_email = 'invalid'
+
+ subject
+
+ expect(user.errors).to be_present
+ expect(user.errors.first[1]).to eq 'is invalid'
+ end
+
+ it 'returns error when email is nil' do
+ user.public_email = nil
+
+ subject
+
+ expect(user.errors).to be_present
+ end
+
+ it 'returns error when email is blank' do
+ user.public_email = ''
+
+ subject
+
+ expect(user.errors).to be_present
+ expect(user.errors.first[1]).to eq 'is invalid'
+ end
+ end
+ end
+
+ context 'when regexp is set as Regexp' do
+ let(:validator) { described_class.new(attributes: [:public_email], regexp: /[0-9]/) }
+
+ it 'allows when value match' do
+ user.public_email = '1'
+
+ subject
+
+ expect(user.errors).to be_empty
+ end
+
+ it 'returns error when value does not match' do
+ subject
+
+ expect(user.errors).to be_present
+ end
+ end
+
+ context 'when regexp is set as String' do
+ it 'raise argument error' do
+ expect { described_class.new( { regexp: 'something' } ) }.to raise_error ArgumentError
+ end
+ end
+
+ context 'when allow_nil is set to true' do
+ let(:validator) { described_class.new(attributes: [:public_email], allow_nil: true) }
+
+ it 'allows when email is nil' do
+ user.public_email = nil
+
+ subject
+
+ expect(user.errors).to be_empty
+ end
+ end
+
+ context 'when allow_blank is set to true' do
+ let(:validator) { described_class.new(attributes: [:public_email], allow_blank: true) }
+
+ it 'allows when email is blank' do
+ user.public_email = ''
+
+ subject
+
+ expect(user.errors).to be_empty
+ end
+ end
+end
diff --git a/spec/validators/public_url_validator_spec.rb b/spec/validators/public_url_validator_spec.rb
index 710dd3dc38e..f6364fb1dd5 100644
--- a/spec/validators/public_url_validator_spec.rb
+++ b/spec/validators/public_url_validator_spec.rb
@@ -1,20 +1,20 @@
require 'spec_helper'
describe PublicUrlValidator do
- include_examples 'url validator examples', described_class::DEFAULT_PROTOCOLS
+ include_examples 'url validator examples', AddressableUrlValidator::DEFAULT_OPTIONS[:schemes]
context 'by default' do
let(:validator) { described_class.new(attributes: [:link_url]) }
let!(:badge) { build(:badge, link_url: 'http://www.example.com') }
- subject { validator.validate_each(badge, :link_url, badge.link_url) }
+ subject { validator.validate(badge) }
it 'blocks urls pointing to localhost' do
badge.link_url = 'https://127.0.0.1'
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
it 'blocks urls pointing to the local network' do
@@ -22,7 +22,7 @@ describe PublicUrlValidator do
subject
- expect(badge.errors.empty?).to be false
+ expect(badge.errors).to be_present
end
end
end
diff --git a/spec/validators/sha_validator_spec.rb b/spec/validators/sha_validator_spec.rb
new file mode 100644
index 00000000000..0a76570f65e
--- /dev/null
+++ b/spec/validators/sha_validator_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ShaValidator do
+ let(:validator) { described_class.new(attributes: [:base_commit_sha]) }
+ let!(:merge_diff) { build(:merge_request_diff) }
+
+ subject { validator.validate_each(merge_diff, :base_commit_sha, value) }
+
+ context 'with empty value' do
+ let(:value) { nil }
+
+ it 'does not add any error if value is empty' do
+ expect(Commit).not_to receive(:valid_hash?)
+
+ subject
+
+ expect(merge_diff.errors).to be_empty
+ end
+ end
+
+ context 'with valid sha' do
+ let(:value) { Digest::SHA1.hexdigest(SecureRandom.hex) }
+
+ it 'does not add any error' do
+ expect(Commit).to receive(:valid_hash?).and_call_original
+
+ subject
+
+ expect(merge_diff.errors).to be_empty
+ end
+ end
+
+ context 'with invalid sha' do
+ let(:value) { 'foo' }
+
+ it 'adds error to the record' do
+ expect(Commit).to receive(:valid_hash?).and_call_original
+ expect(merge_diff.errors).to be_empty
+
+ subject
+
+ expect(merge_diff.errors).not_to be_empty
+ end
+ end
+end
diff --git a/spec/validators/x509_certificate_credentials_validator_spec.rb b/spec/validators/x509_certificate_credentials_validator_spec.rb
new file mode 100644
index 00000000000..24ef68c1fab
--- /dev/null
+++ b/spec/validators/x509_certificate_credentials_validator_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe X509CertificateCredentialsValidator do
+ let(:certificate_data) { File.read('spec/fixtures/x509_certificate.crt') }
+ let(:pkey_data) { File.read('spec/fixtures/x509_certificate_pk.key') }
+
+ let(:validatable) do
+ Class.new do
+ include ActiveModel::Validations
+
+ attr_accessor :certificate, :private_key, :passphrase
+
+ def initialize(certificate, private_key, passphrase = nil)
+ @certificate, @private_key, @passphrase = certificate, private_key, passphrase
+ end
+ end
+ end
+
+ subject(:validator) do
+ described_class.new(certificate: :certificate, pkey: :private_key)
+ end
+
+ it 'is not valid when the certificate is not valid' do
+ record = validatable.new('not a certificate', nil)
+
+ validator.validate(record)
+
+ expect(record.errors[:certificate]).to include('is not a valid X509 certificate.')
+ end
+
+ it 'is not valid without a certificate' do
+ record = validatable.new(nil, nil)
+
+ validator.validate(record)
+
+ expect(record.errors[:certificate]).not_to be_empty
+ end
+
+ context 'when a valid certificate is passed' do
+ let(:record) { validatable.new(certificate_data, nil) }
+
+ it 'does not track an error for the certificate' do
+ validator.validate(record)
+
+ expect(record.errors[:certificate]).to be_empty
+ end
+
+ it 'adds an error when not passing a correct private key' do
+ validator.validate(record)
+
+ expect(record.errors[:private_key]).to include('could not read private key, is the passphrase correct?')
+ end
+
+ it 'has no error when the private key is correct' do
+ record.private_key = pkey_data
+
+ validator.validate(record)
+
+ expect(record.errors).to be_empty
+ end
+ end
+
+ context 'when using a passphrase' do
+ let(:passphrase_certificate_data) { File.read('spec/fixtures/passphrase_x509_certificate.crt') }
+ let(:passphrase_pkey_data) { File.read('spec/fixtures/passphrase_x509_certificate_pk.key') }
+
+ let(:record) { validatable.new(passphrase_certificate_data, passphrase_pkey_data, '5iveL!fe') }
+
+ subject(:validator) do
+ described_class.new(certificate: :certificate, pkey: :private_key, pass: :passphrase)
+ end
+
+ it 'is valid with the correct data' do
+ validator.validate(record)
+
+ expect(record.errors).to be_empty
+ end
+
+ it 'adds an error when the passphrase is wrong' do
+ record.passphrase = 'wrong'
+
+ validator.validate(record)
+
+ expect(record.errors[:private_key]).not_to be_empty
+ end
+ end
+end
diff --git a/spec/views/ci/status/_icon.html.haml_spec.rb b/spec/views/ci/status/_icon.html.haml_spec.rb
new file mode 100644
index 00000000000..626159fc512
--- /dev/null
+++ b/spec/views/ci/status/_icon.html.haml_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'ci/status/_icon' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :private) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when rendering status for build' do
+ let(:build) do
+ create(:ci_build, :success, pipeline: pipeline)
+ end
+
+ context 'when user has ability to see details' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'has link to build details page' do
+ details_path = project_job_path(project, build)
+
+ render_status(build)
+
+ expect(rendered).to have_link(href: details_path)
+ end
+ end
+
+ context 'when user do not have ability to see build details' do
+ before do
+ render_status(build)
+ end
+
+ it 'contains build status text' do
+ expect(rendered).to have_css('.ci-status-icon.ci-status-icon-success')
+ end
+
+ it 'does not contain links' do
+ expect(rendered).not_to have_link
+ end
+ end
+ end
+
+ context 'when rendering status for external job' do
+ context 'when user has ability to see commit status details' do
+ before do
+ project.add_developer(user)
+ end
+
+ context 'status has external target url' do
+ before do
+ external_job = create(:generic_commit_status,
+ status: :running,
+ pipeline: pipeline,
+ target_url: 'http://gitlab.com')
+
+ render_status(external_job)
+ end
+
+ it 'contains valid commit status text' do
+ expect(rendered).to have_css('.ci-status-icon.ci-status-icon-running')
+ end
+
+ it 'has link to external status page' do
+ expect(rendered).to have_link(href: 'http://gitlab.com')
+ end
+ end
+
+ context 'status do not have external target url' do
+ before do
+ external_job = create(:generic_commit_status, status: :canceled)
+
+ render_status(external_job)
+ end
+
+ it 'contains valid commit status text' do
+ expect(rendered).to have_css('.ci-status-icon.ci-status-icon-canceled')
+ end
+
+ it 'has link to external status page' do
+ expect(rendered).not_to have_link
+ end
+ end
+ end
+ end
+
+ def render_status(resource)
+ render 'ci/status/icon', status: resource.detailed_status(user)
+ end
+end
diff --git a/spec/views/groups/_home_panel.html.haml_spec.rb b/spec/views/groups/_home_panel.html.haml_spec.rb
new file mode 100644
index 00000000000..91c5ca261b9
--- /dev/null
+++ b/spec/views/groups/_home_panel.html.haml_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'groups/_home_panel' do
+ let(:group) { create(:group) }
+
+ before do
+ assign(:group, group)
+ end
+
+ it 'renders the group ID' do
+ render
+
+ expect(rendered).to have_content("Group ID: #{group.id}")
+ end
+end
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index 38cfb84f0d5..29e15960fb8 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -12,7 +12,7 @@ describe 'groups/edit.html.haml' do
end
shared_examples_for '"Share with group lock" setting' do |checkbox_options|
- it 'should have the correct label, help text, and checkbox options' do
+ it 'has the correct label, help text, and checkbox options' do
assign(:group, test_group)
allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
index 34e93d929a7..257991549a9 100644
--- a/spec/views/help/index.html.haml_spec.rb
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -31,7 +31,7 @@ describe 'help/index' do
render
expect(rendered).to match '8.0.2'
- expect(rendered).to have_link('8.0.2', href: %r{https://gitlab.com/gitlab-org/gitlab-(ce|ee)/tags/v8.0.2})
+ expect(rendered).to have_link('8.0.2', href: %r{https://gitlab.com/gitlab-org/gitlab-(ce|ee)/-/tags/v8.0.2})
end
it 'shows a link to the commit for pre-releases' do
diff --git a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
index 05c2f61a606..bf63021a7fa 100644
--- a/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_admin.html.haml_spec.rb
@@ -26,6 +26,8 @@ describe 'layouts/nav/sidebar/_admin' do
it_behaves_like 'page has active tab', 'Overview'
end
+ it_behaves_like 'has nav sidebar'
+
context 'on projects' do
before do
allow(controller).to receive(:controller_name).and_return('projects')
diff --git a/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
new file mode 100644
index 00000000000..24b66a0e767
--- /dev/null
+++ b/spec/views/layouts/nav/sidebar/_group.html.haml_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'layouts/nav/sidebar/_group' do
+ let(:group) { create(:group) }
+
+ before do
+ assign(:group, group)
+ end
+
+ it_behaves_like 'has nav sidebar'
+end
diff --git a/spec/views/layouts/nav/sidebar/_instance_statistics.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_instance_statistics.html.haml_spec.rb
new file mode 100644
index 00000000000..7f7f5637035
--- /dev/null
+++ b/spec/views/layouts/nav/sidebar/_instance_statistics.html.haml_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'layouts/nav/sidebar/_instance_statistics' do
+ it_behaves_like 'has nav sidebar'
+end
diff --git a/spec/views/layouts/nav/sidebar/_profile.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_profile.html.haml_spec.rb
new file mode 100644
index 00000000000..6b820ab0b4c
--- /dev/null
+++ b/spec/views/layouts/nav/sidebar/_profile.html.haml_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'layouts/nav/sidebar/_profile' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(view).to receive(:current_user).and_return(user)
+ end
+
+ it_behaves_like 'has nav sidebar'
+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 d9f05e5f94f..c6c10001bc5 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -11,6 +11,8 @@ describe 'layouts/nav/sidebar/_project' do
allow(view).to receive(:can?).and_return(true)
end
+ it_behaves_like 'has nav sidebar'
+
describe 'issue boards' do
it 'has board tab' do
render
@@ -111,4 +113,56 @@ describe 'layouts/nav/sidebar/_project' do
end
end
end
+
+ describe 'ci/cd settings tab' do
+ before do
+ project.update!(archived: project_archived)
+ end
+
+ context 'when project is archived' do
+ let(:project_archived) { true }
+
+ it 'does not show the ci/cd settings tab' do
+ render
+
+ expect(rendered).not_to have_link('CI / CD', href: project_settings_ci_cd_path(project))
+ end
+ end
+
+ context 'when project is active' do
+ let(:project_archived) { false }
+
+ it 'shows the ci/cd settings tab' do
+ render
+
+ expect(rendered).to have_link('CI / CD', href: project_settings_ci_cd_path(project))
+ end
+ end
+ end
+
+ describe 'operations settings tab' do
+ before do
+ project.update!(archived: project_archived)
+ end
+
+ context 'when project is archived' do
+ let(:project_archived) { true }
+
+ it 'does not show the operations settings tab' do
+ render
+
+ expect(rendered).not_to have_link('Operations', href: project_settings_operations_path(project))
+ end
+ end
+
+ context 'when project is active' do
+ let(:project_archived) { false }
+
+ it 'shows the operations settings tab' do
+ render
+
+ expect(rendered).to have_link('Operations', href: project_settings_operations_path(project))
+ end
+ end
+ end
end
diff --git a/spec/views/notify/pipeline_failed_email.text.erb_spec.rb b/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
new file mode 100644
index 00000000000..a7d3dc09fd4
--- /dev/null
+++ b/spec/views/notify/pipeline_failed_email.text.erb_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'notify/pipeline_failed_email.text.erb' do
+ include Devise::Test::ControllerHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:merge_request) { create(:merge_request, :simple, source_project: project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ :failed,
+ project: project,
+ user: user,
+ ref: project.default_branch,
+ sha: project.commit.sha)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+ assign(:merge_request, merge_request)
+ end
+
+ it 'renders the email correctly' do
+ job = create(:ci_build, :failed, pipeline: pipeline, project: pipeline.project)
+
+ render
+
+ expect(rendered).to have_content('Your pipeline has failed')
+ expect(rendered).to have_content(pipeline.project.name)
+ expect(rendered).to have_content(pipeline.git_commit_message.truncate(50))
+ expect(rendered).to have_content(pipeline.commit.author_name)
+ expect(rendered).to have_content("##{pipeline.id}")
+ expect(rendered).to have_content(pipeline.user.name)
+ expect(rendered).to have_content("/-/jobs/#{job.id}/raw")
+ end
+end
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index 908ecb898e4..12925a5ab07 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -45,7 +45,7 @@ describe 'projects/_home_panel' do
context 'badges' do
shared_examples 'show badges' do
- it 'should render the all badges' do
+ it 'renders the all badges' do
render
expect(rendered).to have_selector('.project-badges a')
@@ -70,7 +70,7 @@ describe 'projects/_home_panel' do
context 'has no badges' do
let(:project) { create(:project) }
- it 'should not render any badge' do
+ it 'does not render any badge' do
render
expect(rendered).not_to have_selector('.project-badges')
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
index 1086546c10d..457dd2e940f 100644
--- a/spec/views/projects/commit/_commit_box.html.haml_spec.rb
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -27,7 +27,7 @@ describe 'projects/commit/_commit_box.html.haml' do
render
- expect(rendered).to have_text("Pipeline ##{third_pipeline.id} failed")
+ expect(rendered).to have_text("Pipeline ##{third_pipeline.id} (##{third_pipeline.iid}) failed")
end
end
@@ -40,7 +40,7 @@ describe 'projects/commit/_commit_box.html.haml' do
it 'shows correct pipeline description' do
render
- expect(rendered).to have_text "Pipeline ##{pipeline.id} " \
+ expect(rendered).to have_text "Pipeline ##{pipeline.id} (##{pipeline.iid}) " \
'waiting for manual action'
end
end
diff --git a/spec/views/projects/deployments/_confirm_rollback_modal_spec.html.rb b/spec/views/projects/deployments/_confirm_rollback_modal_spec.html.rb
new file mode 100644
index 00000000000..54ec4f32856
--- /dev/null
+++ b/spec/views/projects/deployments/_confirm_rollback_modal_spec.html.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'projects/deployments/_confirm_rollback_modal' do
+ let(:environment) { create(:environment, :with_review_app) }
+ let(:deployments) { environment.deployments }
+ let(:project) { environment.project }
+
+ before do
+ assign(:environment, environment)
+ assign(:deployments, deployments)
+ assign(:project, project)
+ end
+
+ context 'when re-deploying last deployment' do
+ let(:deployment) { deployments.first }
+
+ before do
+ allow(view).to receive(:deployment).and_return(deployment)
+ end
+
+ it 'shows "re-deploy"' do
+ render
+
+ expect(rendered).to have_selector('h4', text: "Re-deploy environment #{environment.name}?")
+ expect(rendered).to have_selector('p', text: "This action will relaunch the job for commit #{deployment.short_sha}, putting the environment in a previous version. Are you sure you want to continue?")
+ expect(rendered).to have_selector('a.btn-danger', text: 'Re-deploy')
+ end
+
+ it 'links to re-deploying the environment' do
+ expected_link = retry_project_job_path(environment.project, deployment.deployable)
+
+ render
+
+ expect(rendered).to have_selector("a[href='#{expected_link}']", text: 'Re-deploy')
+ end
+ end
+
+ context 'when rolling back to previous deployment' do
+ let(:deployment) { create(:deployment, environment: environment) }
+
+ before do
+ allow(view).to receive(:deployment).and_return(deployment)
+ end
+
+ it 'shows "rollback"' do
+ render
+
+ expect(rendered).to have_selector('h4', text: "Rollback environment #{environment.name}?")
+ expect(rendered).to have_selector('p', text: "This action will run the job defined by staging for commit #{deployment.short_sha}, putting the environment in a previous version. You can revert it by re-deploying the latest version of your application. Are you sure you want to continue?")
+ expect(rendered).to have_selector('a.btn-danger', text: 'Rollback')
+ end
+
+ it 'links to re-deploying the environment' do
+ expected_link = retry_project_job_path(environment.project, deployment.deployable)
+
+ render
+
+ expect(rendered).to have_selector("a[href='#{expected_link}']", text: 'Rollback')
+ end
+ end
+end
diff --git a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb b/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb
deleted file mode 100644
index 02c225292ce..00000000000
--- a/spec/views/projects/issues/_merge_requests_status.html.haml_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-# frozen_string_literal: true
-require 'spec_helper'
-
-describe 'projects/issues/_merge_requests_status.html.haml' do
- it 'shows date of status change in tooltip' do
- merge_request = create(:merge_request, created_at: 1.month.ago)
-
- render partial: 'projects/issues/merge_requests_status',
- locals: { merge_request: merge_request, css_class: '' }
-
- expect(rendered).to match("Opened.*about 1 month ago")
- end
-
- it 'shows only status in tooltip if date is not set' do
- merge_request = create(:merge_request, state: :closed)
-
- render partial: 'projects/issues/merge_requests_status',
- locals: { merge_request: merge_request, css_class: '' }
-
- expect(rendered).to match("Closed")
- end
-end
diff --git a/spec/views/projects/issues/show.html.haml_spec.rb b/spec/views/projects/issues/show.html.haml_spec.rb
index 1d9c6d36ad7..1ca9eaf8fdb 100644
--- a/spec/views/projects/issues/show.html.haml_spec.rb
+++ b/spec/views/projects/issues/show.html.haml_spec.rb
@@ -19,6 +19,7 @@ describe 'projects/issues/show' do
context 'when the issue is closed' do
before do
allow(issue).to receive(:closed?).and_return(true)
+ allow(view).to receive(:current_user).and_return(user)
end
context 'when the issue was moved' do
@@ -28,16 +29,30 @@ describe 'projects/issues/show' do
issue.moved_to = new_issue
end
- it 'shows "Closed (moved)" if an issue has been moved' do
- render
+ context 'when user can see the moved issue' do
+ before do
+ project.add_developer(user)
+ end
- expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ it 'shows "Closed (moved)" if an issue has been moved' do
+ render
+
+ expect(rendered).to have_selector('.status-box-issue-closed:not(.hidden)', text: 'Closed (moved)')
+ end
+
+ it 'links "moved" to the new issue the original issue was moved to' do
+ render
+
+ expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ end
end
- it 'links "moved" to the new issue the original issue was moved to' do
- render
+ context 'when user cannot see moved issue' do
+ it 'does not show moved issue link' do
+ render
- expect(rendered).to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ expect(rendered).not_to have_selector("a[href=\"#{issue_path(new_issue)}\"]", text: 'moved')
+ end
end
end
diff --git a/spec/views/projects/jobs/_build.html.haml_spec.rb b/spec/views/projects/jobs/_build.html.haml_spec.rb
index 1d58891036e..97b25a6976f 100644
--- a/spec/views/projects/jobs/_build.html.haml_spec.rb
+++ b/spec/views/projects/jobs/_build.html.haml_spec.rb
@@ -4,7 +4,7 @@ describe 'projects/ci/jobs/_build' do
include Devise::Test::ControllerHelpers
let(:project) { create(:project, :repository) }
- let(:pipeline) { create(:ci_empty_pipeline, id: 1337, project: project, sha: project.commit.id) }
+ let(:pipeline) { create(:ci_empty_pipeline, id: 1337, iid: 57, project: project, sha: project.commit.id) }
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test', stage_idx: 1, name: 'rspec 0:2', status: :pending) }
before do
@@ -15,14 +15,14 @@ describe 'projects/ci/jobs/_build' do
it 'won\'t include a column with a link to its pipeline by default' do
render partial: 'projects/ci/builds/build', locals: { build: build }
- expect(rendered).not_to have_link('#1337')
- expect(rendered).not_to have_text('#1337 by API')
+ expect(rendered).not_to have_link('#1337 (#57)')
+ expect(rendered).not_to have_text('#1337 (#57) by API')
end
it 'can include a column with a link to its pipeline' do
render partial: 'projects/ci/builds/build', locals: { build: build, pipeline_link: true }
- expect(rendered).to have_link('#1337')
- expect(rendered).to have_text('#1337 by API')
+ expect(rendered).to have_link('#1337 (#57)')
+ expect(rendered).to have_text('#1337 (#57) by API')
end
end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index c13eab30054..529afa03f9c 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -17,7 +17,7 @@ describe 'projects/merge_requests/edit.html.haml' do
source_project: forked_project,
target_project: project,
author: user,
- assignee: user,
+ assignees: [user],
milestone: milestone)
end
@@ -40,7 +40,7 @@ describe 'projects/merge_requests/edit.html.haml' do
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
- expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
end
@@ -52,7 +52,7 @@ describe 'projects/merge_requests/edit.html.haml' do
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
- expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
end
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index d9bda1a3414..23cb319a202 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -53,19 +53,6 @@ describe 'projects/merge_requests/show.html.haml' do
expect(rendered).not_to have_css('.cannot-be-merged')
end
end
-
- context 'when assignee is not allowed to merge' do
- it 'shows a warning icon' do
- reporter = create(:user)
- project.add_reporter(reporter)
- closed_merge_request.update(assignee_id: reporter.id)
- assign(:issuable_sidebar, serialize_issuable_sidebar(user, project, closed_merge_request))
-
- render
-
- expect(rendered).to have_css('.cannot-be-merged')
- end
- end
end
context 'when the merge request is closed' do
diff --git a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
index 8a9ab02eaca..ae47f364296 100644
--- a/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
+++ b/spec/views/projects/notes/_more_actions_dropdown.html.haml_spec.rb
@@ -12,10 +12,10 @@ describe 'projects/notes/_more_actions_dropdown' do
assign(:project, project)
end
- it 'shows Report abuse to GitLab button if not editable and not current users comment' do
+ it 'shows Report abuse to admin button if not editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: false, note: note
- expect(rendered).to have_link('Report abuse to GitLab')
+ expect(rendered).to have_link('Report abuse to admin')
end
it 'does not show the More actions button if not editable and current users comment' do
@@ -24,10 +24,10 @@ describe 'projects/notes/_more_actions_dropdown' do
expect(rendered).not_to have_selector('.dropdown.more-actions')
end
- it 'shows Report abuse to GitLab and Delete buttons if editable and not current users comment' do
+ it 'shows Report abuse to admin and Delete buttons if editable and not current users comment' do
render 'projects/notes/more_actions_dropdown', current_user: not_author_user, note_editable: true, note: note
- expect(rendered).to have_link('Report abuse to GitLab')
+ expect(rendered).to have_link('Report abuse to admin')
expect(rendered).to have_link('Delete comment')
end
diff --git a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
index 2a2539c80b5..ff2d491539b 100644
--- a/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
+++ b/spec/views/projects/settings/ci_cd/_autodevops_form.html.haml_spec.rb
@@ -5,6 +5,7 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
before do
assign :project, project
+ allow(view).to receive(:auto_devops_enabled) { true }
end
it 'shows a warning message about Kubernetes cluster' do
@@ -12,4 +13,14 @@ describe 'projects/settings/ci_cd/_autodevops_form' do
expect(rendered).to have_text('You must add a Kubernetes cluster integration to this project with a domain in order for your deployment strategy to work correctly.')
end
+
+ context 'when the project has an available kubernetes cluster' do
+ let!(:cluster) { create(:cluster, cluster_type: :project_type, projects: [project]) }
+
+ it 'does not show a warning message' do
+ render
+
+ expect(rendered).not_to have_text('You must add a Kubernetes cluster')
+ end
+ end
end
diff --git a/spec/views/projects/settings/operations/show.html.haml_spec.rb b/spec/views/projects/settings/operations/show.html.haml_spec.rb
index 8e34521c7c8..6762fe3759b 100644
--- a/spec/views/projects/settings/operations/show.html.haml_spec.rb
+++ b/spec/views/projects/settings/operations/show.html.haml_spec.rb
@@ -18,6 +18,7 @@ describe 'projects/settings/operations/show' do
allow(view).to receive(:error_tracking_setting)
.and_return(error_tracking_setting)
allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:incident_management_available?) { false }
end
let!(:error_tracking_setting) do
@@ -30,7 +31,6 @@ describe 'projects/settings/operations/show' do
expect(rendered).to have_content _('Error Tracking')
expect(rendered).to have_content _('To link Sentry to GitLab, enter your Sentry URL and Auth Token')
- expect(rendered).to have_content _('Active')
end
end
end
diff --git a/spec/views/projects/tags/index.html.haml_spec.rb b/spec/views/projects/tags/index.html.haml_spec.rb
index cb97d17988c..34355e27544 100644
--- a/spec/views/projects/tags/index.html.haml_spec.rb
+++ b/spec/views/projects/tags/index.html.haml_spec.rb
@@ -1,20 +1,53 @@
require 'spec_helper'
describe 'projects/tags/index' do
- let(:project) { create(:project, :repository) }
+ let(:project) { create(:project, :repository) }
+ let(:tags) { TagsFinder.new(project.repository, {}).execute }
+ let(:git_tag) { project.repository.tags.last }
+ let(:release) { create(:release, project: project, sha: git_tag.target_commit.sha) }
+ let(:pipeline) { create(:ci_pipeline, :success, project: project, ref: git_tag.name, sha: release.sha) }
before do
assign(:project, project)
assign(:repository, project.repository)
- assign(:tags, [])
+ assign(:releases, project.releases)
+ assign(:tags, Kaminari.paginate_array(tags).page(0))
+ assign(:tags_pipelines, { git_tag.name => pipeline })
allow(view).to receive(:current_ref).and_return('master')
- allow(view).to receive(:can?).and_return(false)
+ allow(view).to receive(:current_user).and_return(project.namespace.owner)
end
it 'defaults sort dropdown toggle to last updated' do
render
-
expect(rendered).to have_button('Last updated')
end
+
+ context 'when the most recent build for a tag has artifacts' do
+ let!(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ it 'renders the Artifacts section in the download list' do
+ render
+ expect(rendered).to have_selector('li', text: 'Artifacts')
+ end
+
+ it 'renders artifact download links' do
+ render
+ expect(rendered).to have_link(href: latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: 'test'))
+ end
+ end
+
+ context 'when the most recent build for a tag has expired artifacts' do
+ let!(:build) { create(:ci_build, :success, :expired, :artifacts, pipeline: pipeline) }
+
+ it 'does not render the Artifacts section in the download list' do
+ render
+ expect(rendered).not_to have_selector('li', text: 'Artifacts')
+ end
+
+ it 'does not render artifact download links' do
+ render
+ expect(rendered).not_to have_link(href: latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: 'test'))
+ end
+ end
end
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 3b098320ad7..5bb0173ab89 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -7,6 +7,8 @@ describe 'projects/tree/show' do
let(:repository) { project.repository }
before do
+ stub_feature_flags(vue_file_list: false)
+
assign(:project, project)
assign(:repository, repository)
assign(:lfs_blob_ids, [])
diff --git a/spec/views/shared/_label_row.html.haml.rb b/spec/views/shared/_label_row.html.haml.rb
new file mode 100644
index 00000000000..a58d5efc1e3
--- /dev/null
+++ b/spec/views/shared/_label_row.html.haml.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe 'shared/_label_row.html.haml' do
+ label_types = {
+ 'project label': :label,
+ 'group label': :group_label
+ }
+
+ label_types.each do |label_type, label_factory|
+ let!(:label) { create(label_factory) }
+
+ context "for a #{label_type}" do
+ it 'has a non-linked label title' do
+ render 'shared/label_row', label: label
+
+ expect(rendered).not_to have_css('a', text: label.title)
+ end
+
+ it "has Issues link for #{label_type}" do
+ render 'shared/label_row', label: label
+
+ expect(rendered).to have_css('a', text: 'Issues')
+ end
+
+ it "has Merge request link for #{label_type}" do
+ render 'shared/label_row', label: label
+
+ expect(rendered).to have_css('a', text: 'Merge requests')
+ end
+ end
+ end
+end
diff --git a/spec/views/shared/milestones/_issuables.html.haml.rb b/spec/views/shared/milestones/_issuables.html.haml.rb
index 4769d569548..cbbb984935f 100644
--- a/spec/views/shared/milestones/_issuables.html.haml.rb
+++ b/spec/views/shared/milestones/_issuables.html.haml.rb
@@ -11,12 +11,12 @@ describe 'shared/milestones/_issuables.html.haml' do
stub_template 'shared/milestones/_issuable.html.haml' => ''
end
- it 'should show the issuables count if show_counter is true' do
+ it 'shows the issuables count if show_counter is true' do
render 'shared/milestones/issuables', show_counter: true
expect(rendered).to have_content('100')
end
- it 'should not show the issuables count if show_counter is false' do
+ it 'does not show the issuables count if show_counter is false' do
render 'shared/milestones/issuables', show_counter: false
expect(rendered).not_to have_content('100')
end
@@ -24,7 +24,7 @@ describe 'shared/milestones/_issuables.html.haml' do
describe 'a high issuables count' do
let(:issuables_size) { 1000 }
- it 'should show a delimited number if show_counter is true' do
+ it 'shows a delimited number if show_counter is true' do
render 'shared/milestones/issuables', show_counter: true
expect(rendered).to have_content('1,000')
end
diff --git a/spec/views/shared/projects/_project.html.haml_spec.rb b/spec/views/shared/projects/_project.html.haml_spec.rb
index 3b14045e61f..dc223861037 100644
--- a/spec/views/shared/projects/_project.html.haml_spec.rb
+++ b/spec/views/shared/projects/_project.html.haml_spec.rb
@@ -8,13 +8,13 @@ describe 'shared/projects/_project.html.haml' do
allow(view).to receive(:can?) { true }
end
- it 'should render creator avatar if project has a creator' do
+ it 'renders creator avatar if project has a creator' do
render 'shared/projects/project', use_creator_avatar: true, project: project
expect(rendered).to have_selector('img.avatar')
end
- it 'should render a generic avatar if project does not have a creator' do
+ it 'renders a generic avatar if project does not have a creator' do
project.creator = nil
render 'shared/projects/project', use_creator_avatar: true, project: project
diff --git a/spec/workers/admin_email_worker_spec.rb b/spec/workers/admin_email_worker_spec.rb
index 27687f069ea..f72b932423f 100644
--- a/spec/workers/admin_email_worker_spec.rb
+++ b/spec/workers/admin_email_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AdminEmailWorker do
diff --git a/spec/workers/archive_trace_worker_spec.rb b/spec/workers/archive_trace_worker_spec.rb
index 7244ad4f199..368ed3f3db1 100644
--- a/spec/workers/archive_trace_worker_spec.rb
+++ b/spec/workers/archive_trace_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ArchiveTraceWorker do
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index d095138f6b7..4c02278de64 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe AuthorizedProjectsWorker do
diff --git a/spec/workers/auto_merge_process_worker_spec.rb b/spec/workers/auto_merge_process_worker_spec.rb
new file mode 100644
index 00000000000..616727ce5ca
--- /dev/null
+++ b/spec/workers/auto_merge_process_worker_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AutoMergeProcessWorker do
+ describe '#perform' do
+ subject { described_class.new.perform(merge_request&.id) }
+
+ context 'when merge request is found' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'executes AutoMergeService' do
+ expect_next_instance_of(AutoMergeService) do |auto_merge|
+ expect(auto_merge).to receive(:process)
+ end
+
+ subject
+ end
+ end
+
+ context 'when merge request is not found' do
+ let(:merge_request) { nil }
+
+ it 'does not execute AutoMergeService' do
+ expect(AutoMergeService).not_to receive(:new)
+
+ subject
+ end
+ end
+ end
+end
diff --git a/spec/workers/background_migration_worker_spec.rb b/spec/workers/background_migration_worker_spec.rb
index 3bd072e7125..746c858609f 100644
--- a/spec/workers/background_migration_worker_spec.rb
+++ b/spec/workers/background_migration_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BackgroundMigrationWorker, :sidekiq, :clean_gitlab_redis_shared_state do
diff --git a/spec/workers/build_coverage_worker_spec.rb b/spec/workers/build_coverage_worker_spec.rb
index ba20488f663..25686ae68ca 100644
--- a/spec/workers/build_coverage_worker_spec.rb
+++ b/spec/workers/build_coverage_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildCoverageWorker do
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index ccb26849e67..4adb795b1d6 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildFinishedWorker do
@@ -15,6 +17,7 @@ describe BuildFinishedWorker do
expect_any_instance_of(BuildCoverageWorker).to receive(:perform)
expect(BuildHooksWorker).to receive(:perform_async)
expect(ArchiveTraceWorker).to receive(:perform_async)
+ expect(ExpirePipelineCacheWorker).to receive(:perform_async)
described_class.new.perform(build.id)
end
diff --git a/spec/workers/build_hooks_worker_spec.rb b/spec/workers/build_hooks_worker_spec.rb
index 97654a93f5c..59b252a8be3 100644
--- a/spec/workers/build_hooks_worker_spec.rb
+++ b/spec/workers/build_hooks_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildHooksWorker do
diff --git a/spec/workers/build_success_worker_spec.rb b/spec/workers/build_success_worker_spec.rb
index 5eb9709ded9..ffe8796ded9 100644
--- a/spec/workers/build_success_worker_spec.rb
+++ b/spec/workers/build_success_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildSuccessWorker do
@@ -13,6 +15,7 @@ describe BuildSuccessWorker do
let!(:build) { create(:ci_build, :deploy_to_production) }
before do
+ allow(Deployments::FinishedWorker).to receive(:perform_async)
Deployment.delete_all
build.reload
end
diff --git a/spec/workers/build_trace_sections_worker_spec.rb b/spec/workers/build_trace_sections_worker_spec.rb
index 45243f45547..97fc0a2da0c 100644
--- a/spec/workers/build_trace_sections_worker_spec.rb
+++ b/spec/workers/build_trace_sections_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe BuildTraceSectionsWorker do
diff --git a/spec/workers/ci/archive_traces_cron_worker_spec.rb b/spec/workers/ci/archive_traces_cron_worker_spec.rb
index 478fb7d2c0f..eca6cf5235f 100644
--- a/spec/workers/ci/archive_traces_cron_worker_spec.rb
+++ b/spec/workers/ci/archive_traces_cron_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::ArchiveTracesCronWorker do
diff --git a/spec/workers/ci/build_prepare_worker_spec.rb b/spec/workers/ci/build_prepare_worker_spec.rb
new file mode 100644
index 00000000000..9f76696ee66
--- /dev/null
+++ b/spec/workers/ci/build_prepare_worker_spec.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::BuildPrepareWorker do
+ subject { described_class.new.perform(build_id) }
+
+ context 'build exists' do
+ let(:build) { create(:ci_build) }
+ let(:build_id) { build.id }
+ let(:service) { double(execute: true) }
+
+ it 'calls the prepare build service' do
+ expect(Ci::PrepareBuildService).to receive(:new).with(build).and_return(service)
+ expect(service).to receive(:execute).once
+
+ subject
+ end
+ end
+
+ context 'build does not exist' do
+ let(:build_id) { -1 }
+
+ it 'does not attempt to prepare the build' do
+ expect(Ci::PrepareBuildService).not_to receive(:new)
+
+ subject
+ end
+ end
+end
diff --git a/spec/workers/ci/build_schedule_worker_spec.rb b/spec/workers/ci/build_schedule_worker_spec.rb
index 4a3fe84d7f7..647f9763fed 100644
--- a/spec/workers/ci/build_schedule_worker_spec.rb
+++ b/spec/workers/ci/build_schedule_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Ci::BuildScheduleWorker do
diff --git a/spec/workers/cluster_configure_worker_spec.rb b/spec/workers/cluster_configure_worker_spec.rb
index 6918ee3d7d8..975088f3ee6 100644
--- a/spec/workers/cluster_configure_worker_spec.rb
+++ b/spec/workers/cluster_configure_worker_spec.rb
@@ -5,55 +5,57 @@ require 'spec_helper'
describe ClusterConfigureWorker, '#perform' do
let(:worker) { described_class.new }
- context 'when group cluster' do
- let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
- let(:group) { cluster.group }
+ shared_examples 'configured cluster' do
+ it 'creates a namespace' do
+ expect(Clusters::RefreshService).to receive(:create_or_update_namespaces_for_cluster).with(cluster).once
+
+ worker.perform(cluster.id)
+ end
+ end
- context 'when group has no projects' do
- it 'does not create a namespace' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).not_to receive(:execute)
+ shared_examples 'unconfigured cluster' do
+ it 'does not create a namespace' do
+ expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster)
- worker.perform(cluster.id)
- end
+ worker.perform(cluster.id)
end
+ end
+
+ context 'group cluster' do
+ let(:cluster) { create(:cluster, :group, :provided_by_gcp) }
+ let(:group) { cluster.group }
context 'when group has a project' do
let!(:project) { create(:project, group: group) }
- it 'creates a namespace for the project' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
-
- worker.perform(cluster.id)
- end
+ it_behaves_like 'unconfigured cluster'
end
context 'when group has project in a sub-group' do
let!(:subgroup) { create(:group, parent: group) }
let!(:project) { create(:project, group: subgroup) }
- it 'creates a namespace for the project' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute).once
-
- worker.perform(cluster.id)
- end
+ it_behaves_like 'unconfigured cluster'
end
end
context 'when provider type is gcp' do
- let(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
- it 'configures kubernetes platform' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
-
- described_class.new.perform(cluster.id)
- end
+ it_behaves_like 'configured cluster'
end
context 'when provider type is user' do
- let(:cluster) { create(:cluster, :project, :provided_by_user) }
+ let!(:cluster) { create(:cluster, :project, :provided_by_user) }
+
+ it_behaves_like 'configured cluster'
+ end
+
+ context 'when cluster is not managed' do
+ let(:cluster) { create(:cluster, :not_managed) }
- it 'configures kubernetes platform' do
- expect_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
+ it 'does not configure the cluster' do
+ expect(Clusters::RefreshService).not_to receive(:create_or_update_namespaces_for_cluster)
described_class.new.perform(cluster.id)
end
diff --git a/spec/workers/cluster_project_configure_worker_spec.rb b/spec/workers/cluster_project_configure_worker_spec.rb
new file mode 100644
index 00000000000..2ac9d0f61b4
--- /dev/null
+++ b/spec/workers/cluster_project_configure_worker_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ClusterProjectConfigureWorker, '#perform' do
+ let(:worker) { described_class.new }
+ let(:cluster) { create(:cluster, :project) }
+
+ it 'configures the cluster' do
+ expect(Clusters::RefreshService).to receive(:create_or_update_namespaces_for_project)
+
+ described_class.new.perform(cluster.projects.first.id)
+ end
+end
diff --git a/spec/workers/cluster_provision_worker_spec.rb b/spec/workers/cluster_provision_worker_spec.rb
index da32f29fec0..9cc2ad12bfc 100644
--- a/spec/workers/cluster_provision_worker_spec.rb
+++ b/spec/workers/cluster_provision_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ClusterProvisionWorker do
diff --git a/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb
index 2e2e9afd25a..a9ffdfb085e 100644
--- a/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb
+++ b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ClusterWaitForIngressIpAddressWorker do
diff --git a/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb
new file mode 100644
index 00000000000..aaf5c9defc4
--- /dev/null
+++ b/spec/workers/clusters/applications/wait_for_uninstall_app_worker_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Applications::WaitForUninstallAppWorker, '#perform' do
+ let(:app) { create(:clusters_applications_helm) }
+ let(:app_name) { app.name }
+ let(:app_id) { app.id }
+
+ subject { described_class.new.perform(app_name, app_id) }
+
+ context 'app exists' do
+ let(:service) { instance_double(Clusters::Applications::CheckUninstallProgressService) }
+
+ it 'calls the check service' do
+ expect(Clusters::Applications::CheckUninstallProgressService).to receive(:new).with(app).and_return(service)
+ expect(service).to receive(:execute).once
+
+ subject
+ end
+ end
+
+ context 'app does not exist' do
+ let(:app_id) { 0 }
+
+ it 'does not call the check service' do
+ expect(Clusters::Applications::CheckUninstallProgressService).not_to receive(:new)
+
+ expect { subject }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/spec/workers/concerns/application_worker_spec.rb b/spec/workers/concerns/application_worker_spec.rb
index 901d77178bc..ae5244e2f62 100644
--- a/spec/workers/concerns/application_worker_spec.rb
+++ b/spec/workers/concerns/application_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ApplicationWorker do
diff --git a/spec/workers/concerns/cluster_queue_spec.rb b/spec/workers/concerns/cluster_queue_spec.rb
index 4118b9aa194..732d55dfbde 100644
--- a/spec/workers/concerns/cluster_queue_spec.rb
+++ b/spec/workers/concerns/cluster_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ClusterQueue do
diff --git a/spec/workers/concerns/cronjob_queue_spec.rb b/spec/workers/concerns/cronjob_queue_spec.rb
index c042a52f41f..cf4d47b7500 100644
--- a/spec/workers/concerns/cronjob_queue_spec.rb
+++ b/spec/workers/concerns/cronjob_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CronjobQueue do
diff --git a/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb b/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb
index 4b9aa9a7ef8..200cdffd560 100644
--- a/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/notify_upon_death_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::NotifyUponDeath do
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
index 9c187bead0a..51b685b5792 100644
--- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::ObjectImporter do
diff --git a/spec/workers/concerns/gitlab/github_import/queue_spec.rb b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
index a96f583aff7..d262bc2e05c 100644
--- a/spec/workers/concerns/gitlab/github_import/queue_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Queue do
diff --git a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
index 8de4059c4ae..294eacf09ab 100644
--- a/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/rescheduling_methods_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::ReschedulingMethods do
diff --git a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
index d85a87f2cb0..f9081a875b5 100644
--- a/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/stage_methods_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::StageMethods do
diff --git a/spec/workers/concerns/pipeline_background_queue_spec.rb b/spec/workers/concerns/pipeline_background_queue_spec.rb
index 24c0a3c6a20..78ceafb359f 100644
--- a/spec/workers/concerns/pipeline_background_queue_spec.rb
+++ b/spec/workers/concerns/pipeline_background_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineBackgroundQueue do
diff --git a/spec/workers/concerns/pipeline_queue_spec.rb b/spec/workers/concerns/pipeline_queue_spec.rb
index a312b307fce..eedfceb8bf0 100644
--- a/spec/workers/concerns/pipeline_queue_spec.rb
+++ b/spec/workers/concerns/pipeline_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineQueue do
diff --git a/spec/workers/concerns/project_import_options_spec.rb b/spec/workers/concerns/project_import_options_spec.rb
index 3699fd83a9a..c5fbcfb5fb0 100644
--- a/spec/workers/concerns/project_import_options_spec.rb
+++ b/spec/workers/concerns/project_import_options_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectImportOptions do
diff --git a/spec/workers/concerns/repository_check_queue_spec.rb b/spec/workers/concerns/repository_check_queue_spec.rb
index d2eeecfc9a8..55ed71f124c 100644
--- a/spec/workers/concerns/repository_check_queue_spec.rb
+++ b/spec/workers/concerns/repository_check_queue_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryCheckQueue do
diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb
index ce38cde9208..37fadd6ac02 100644
--- a/spec/workers/concerns/waitable_worker_spec.rb
+++ b/spec/workers/concerns/waitable_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe WaitableWorker do
diff --git a/spec/workers/create_gpg_signature_worker_spec.rb b/spec/workers/create_gpg_signature_worker_spec.rb
index f5479e57260..ae09b4b77f1 100644
--- a/spec/workers/create_gpg_signature_worker_spec.rb
+++ b/spec/workers/create_gpg_signature_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CreateGpgSignatureWorker do
diff --git a/spec/workers/create_note_diff_file_worker_spec.rb b/spec/workers/create_note_diff_file_worker_spec.rb
index 0ac946a1232..e35aaa7d593 100644
--- a/spec/workers/create_note_diff_file_worker_spec.rb
+++ b/spec/workers/create_note_diff_file_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CreateNoteDiffFileWorker do
diff --git a/spec/workers/create_pipeline_worker_spec.rb b/spec/workers/create_pipeline_worker_spec.rb
index 02cb0f46cb4..62a17da80c0 100644
--- a/spec/workers/create_pipeline_worker_spec.rb
+++ b/spec/workers/create_pipeline_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe CreatePipelineWorker do
diff --git a/spec/workers/delete_diff_files_worker_spec.rb b/spec/workers/delete_diff_files_worker_spec.rb
index e0edd313922..9f8b20df48e 100644
--- a/spec/workers/delete_diff_files_worker_spec.rb
+++ b/spec/workers/delete_diff_files_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeleteDiffFilesWorker do
diff --git a/spec/workers/delete_merged_branches_worker_spec.rb b/spec/workers/delete_merged_branches_worker_spec.rb
index 39009d9e4b2..a218ca921d9 100644
--- a/spec/workers/delete_merged_branches_worker_spec.rb
+++ b/spec/workers/delete_merged_branches_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeleteMergedBranchesWorker do
diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb
index 06d9e125105..c963b886e62 100644
--- a/spec/workers/delete_user_worker_spec.rb
+++ b/spec/workers/delete_user_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DeleteUserWorker do
diff --git a/spec/workers/deployments/finished_worker_spec.rb b/spec/workers/deployments/finished_worker_spec.rb
new file mode 100644
index 00000000000..df62821e2cd
--- /dev/null
+++ b/spec/workers/deployments/finished_worker_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Deployments::FinishedWorker do
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before do
+ allow(ProjectServiceWorker).to receive(:perform_async)
+ end
+
+ it 'executes project services for deployment_hooks' do
+ deployment = create(:deployment)
+ project = deployment.project
+ service = create(:service, type: 'SlackService', project: project, deployment_events: true, active: true)
+
+ worker.perform(deployment.id)
+
+ expect(ProjectServiceWorker).to have_received(:perform_async).with(service.id, an_instance_of(Hash))
+ end
+
+ it 'does not execute an inactive service' do
+ deployment = create(:deployment)
+ project = deployment.project
+ create(:service, type: 'SlackService', project: project, deployment_events: true, active: false)
+
+ worker.perform(deployment.id)
+
+ expect(ProjectServiceWorker).not_to have_received(:perform_async)
+ end
+
+ it 'does nothing if a deployment with the given id does not exist' do
+ worker.perform(0)
+
+ expect(ProjectServiceWorker).not_to have_received(:perform_async)
+ end
+ end
+end
diff --git a/spec/workers/deployments/success_worker_spec.rb b/spec/workers/deployments/success_worker_spec.rb
index ba7d45eca01..1c68922b03d 100644
--- a/spec/workers/deployments/success_worker_spec.rb
+++ b/spec/workers/deployments/success_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Deployments::SuccessWorker do
diff --git a/spec/workers/detect_repository_languages_worker_spec.rb b/spec/workers/detect_repository_languages_worker_spec.rb
index ff3878fbc8e..755eb8dbf6b 100644
--- a/spec/workers/detect_repository_languages_worker_spec.rb
+++ b/spec/workers/detect_repository_languages_worker_spec.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe DetectRepositoryLanguagesWorker do
set(:project) { create(:project) }
- let(:user) { project.owner }
subject { described_class.new }
@@ -12,19 +13,13 @@ describe DetectRepositoryLanguagesWorker do
allow(::Projects::DetectRepositoryLanguagesService).to receive(:new).and_return(service)
expect(service).to receive(:execute)
- subject.perform(project.id, user.id)
+ subject.perform(project.id)
end
context 'when invalid ids are used' do
it 'does not raise when the project could not be found' do
expect do
- subject.perform(-1, user.id)
- end.not_to raise_error
- end
-
- it 'does not raise when the user could not be found' do
- expect do
- subject.perform(project.id, -1)
+ subject.perform(-1)
end.not_to raise_error
end
end
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index 045135255d6..f8a31fcdee6 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe EmailReceiverWorker, :mailer do
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index 05b4fb49ea3..0f87df89c29 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe EmailsOnPushWorker, :mailer do
diff --git a/spec/workers/every_sidekiq_worker_spec.rb b/spec/workers/every_sidekiq_worker_spec.rb
index ebe02373275..8fddd8540ef 100644
--- a/spec/workers/every_sidekiq_worker_spec.rb
+++ b/spec/workers/every_sidekiq_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe 'Every Sidekiq worker' do
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
index 27995cf1611..74d6b5605d1 100644
--- a/spec/workers/expire_build_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ExpireBuildArtifactsWorker do
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
index e1a56c72162..39f676f1057 100644
--- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ExpireBuildInstanceArtifactsWorker do
@@ -19,7 +21,7 @@ describe ExpireBuildInstanceArtifactsWorker do
end
it 'does remove files' do
- expect(build.reload.artifacts_file.exists?).to be_falsey
+ expect(build.reload.artifacts_file.present?).to be_falsey
end
it 'does remove the job artifact record' do
@@ -38,7 +40,7 @@ describe ExpireBuildInstanceArtifactsWorker do
end
it 'does not remove files' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
+ expect(build.reload.artifacts_file.present?).to be_truthy
end
it 'does not remove the job artifact record' do
@@ -54,7 +56,7 @@ describe ExpireBuildInstanceArtifactsWorker do
end
it 'does not remove files' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
+ expect(build.reload.artifacts_file.present?).to be_truthy
end
it 'does not remove the job artifact record' do
diff --git a/spec/workers/expire_job_cache_worker_spec.rb b/spec/workers/expire_job_cache_worker_spec.rb
index 1b614342a18..6ac285ca944 100644
--- a/spec/workers/expire_job_cache_worker_spec.rb
+++ b/spec/workers/expire_job_cache_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ExpireJobCacheWorker do
diff --git a/spec/workers/expire_pipeline_cache_worker_spec.rb b/spec/workers/expire_pipeline_cache_worker_spec.rb
index 54c9a69d329..5652f5e8685 100644
--- a/spec/workers/expire_pipeline_cache_worker_spec.rb
+++ b/spec/workers/expire_pipeline_cache_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ExpirePipelineCacheWorker do
@@ -7,40 +9,17 @@ describe ExpirePipelineCacheWorker do
subject { described_class.new }
describe '#perform' do
- it 'invalidates Etag caching for project pipelines path' do
- pipelines_path = "/#{project.full_path}/pipelines.json"
- new_mr_pipelines_path = "/#{project.full_path}/merge_requests/new.json"
- pipeline_path = "/#{project.full_path}/pipelines/#{pipeline.id}.json"
-
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipelines_path)
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(new_mr_pipelines_path)
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(pipeline_path)
-
- subject.perform(pipeline.id)
- end
-
- it 'invalidates Etag caching for merge request pipelines if pipeline runs on any commit of that source branch' do
- pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master')
- merge_request = create(:merge_request, source_project: project, source_branch: pipeline.ref)
- merge_request_pipelines_path = "/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines.json"
-
- allow_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch)
- expect_any_instance_of(Gitlab::EtagCaching::Store).to receive(:touch).with(merge_request_pipelines_path)
+ it 'executes the service' do
+ expect_any_instance_of(Ci::ExpirePipelineCacheService).to receive(:execute).with(pipeline).and_call_original
subject.perform(pipeline.id)
end
it "doesn't do anything if the pipeline not exist" do
+ expect_any_instance_of(Ci::ExpirePipelineCacheService).not_to receive(:execute)
expect_any_instance_of(Gitlab::EtagCaching::Store).not_to receive(:touch)
subject.perform(617748)
end
-
- it 'updates the cached status for a project' do
- expect(Gitlab::Cache::Ci::ProjectPipelineStatus).to receive(:update_for_pipeline)
- .with(pipeline)
-
- subject.perform(pipeline.id)
- end
end
end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 4895a968d6e..cc1c23bb9e7 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'fileutils'
require 'spec_helper'
@@ -113,6 +115,19 @@ describe GitGarbageCollectWorker do
end
end
+ context "pack_refs" do
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
+ it "calls Gitaly" do
+ expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:pack_refs)
+ .and_return(nil)
+
+ subject.perform(project.id, :pack_refs, lease_key, lease_uuid)
+ end
+ end
+
context "repack_incremental" do
before do
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
diff --git a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
index fc7aafbc0c9..b1647d8c7df 100644
--- a/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/advance_stage_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::AdvanceStageWorker, :clean_gitlab_redis_shared_state do
diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
index 5b1c6b6010a..42d69ff6166 100644
--- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::ImportDiffNoteWorker do
diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
index ab070d6d081..06a573e16b7 100644
--- a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::ImportIssueWorker do
diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
index 3a30f06bb2d..5110c3ff11b 100644
--- a/spec/workers/gitlab/github_import/import_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::ImportNoteWorker do
diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
index 3cccd7cab21..d46e381fc51 100644
--- a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::ImportPullRequestWorker do
diff --git a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
index 7ff133f1049..fa4ded8e42f 100644
--- a/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/refresh_import_jid_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::RefreshImportJidWorker do
diff --git a/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
index 91e0cddb5d8..35a856802c2 100644
--- a/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/finish_import_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Stage::FinishImportWorker do
diff --git a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
index ad6154cc4a4..0c7fc2a164e 100644
--- a/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_base_data_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Stage::ImportBaseDataWorker do
diff --git a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
index ab347f5b75b..5d96f562c30 100644
--- a/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_issues_and_diff_notes_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Stage::ImportIssuesAndDiffNotesWorker do
diff --git a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
index b19884d7991..e7c9dabb292 100644
--- a/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_lfs_objects_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Stage::ImportLfsObjectsWorker do
diff --git a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
index 94cff9e4e80..90590a45900 100644
--- a/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_notes_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Stage::ImportNotesWorker do
diff --git a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
index 1fbb073a34a..15d485f1018 100644
--- a/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_pull_requests_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Stage::ImportPullRequestsWorker do
diff --git a/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb b/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
index adab535ac05..6d47d73b92e 100644
--- a/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/stage/import_repository_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Gitlab::GithubImport::Stage::ImportRepositoryWorker do
diff --git a/spec/workers/gitlab_shell_worker_spec.rb b/spec/workers/gitlab_shell_worker_spec.rb
index 6b222af454d..0758cfc4ee2 100644
--- a/spec/workers/gitlab_shell_worker_spec.rb
+++ b/spec/workers/gitlab_shell_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabShellWorker do
diff --git a/spec/workers/gitlab_usage_ping_worker_spec.rb b/spec/workers/gitlab_usage_ping_worker_spec.rb
index 49b4e04dc7c..aff5d112cdd 100644
--- a/spec/workers/gitlab_usage_ping_worker_spec.rb
+++ b/spec/workers/gitlab_usage_ping_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GitlabUsagePingWorker do
diff --git a/spec/workers/group_destroy_worker_spec.rb b/spec/workers/group_destroy_worker_spec.rb
index a170c84ab12..90a4150a31a 100644
--- a/spec/workers/group_destroy_worker_spec.rb
+++ b/spec/workers/group_destroy_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe GroupDestroyWorker do
diff --git a/spec/workers/hashed_storage/migrator_worker_spec.rb b/spec/workers/hashed_storage/migrator_worker_spec.rb
index a85f820a3eb..a318cdd003e 100644
--- a/spec/workers/hashed_storage/migrator_worker_spec.rb
+++ b/spec/workers/hashed_storage/migrator_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe HashedStorage::MigratorWorker do
diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/hashed_storage/project_migrate_worker_spec.rb
index 333eb6a0569..f266c7dbe8c 100644
--- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb
+++ b/spec/workers/hashed_storage/project_migrate_worker_spec.rb
@@ -1,6 +1,8 @@
+# frozen_string_literal: true
+
require 'spec_helper'
-describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do
+describe HashedStorage::ProjectMigrateWorker, :clean_gitlab_redis_shared_state do
include ExclusiveLeaseHelpers
describe '#perform' do
diff --git a/spec/workers/hashed_storage/project_rollback_worker_spec.rb b/spec/workers/hashed_storage/project_rollback_worker_spec.rb
new file mode 100644
index 00000000000..d833553c0ec
--- /dev/null
+++ b/spec/workers/hashed_storage/project_rollback_worker_spec.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe HashedStorage::ProjectRollbackWorker, :clean_gitlab_redis_shared_state do
+ include ExclusiveLeaseHelpers
+
+ describe '#perform' do
+ let(:project) { create(:project, :empty_repo) }
+ let(:lease_key) { "project_migrate_hashed_storage_worker:#{project.id}" }
+ let(:lease_timeout) { described_class::LEASE_TIMEOUT }
+ let(:rollback_service) { ::Projects::HashedStorage::RollbackService }
+
+ it 'skips when project no longer exists' do
+ expect(rollback_service).not_to receive(:new)
+
+ subject.perform(-1)
+ end
+
+ it 'skips when project is pending delete' do
+ pending_delete_project = create(:project, :empty_repo, pending_delete: true)
+
+ expect(rollback_service).not_to receive(:new)
+
+ subject.perform(pending_delete_project.id)
+ end
+
+ it 'delegates rollback to service class when have exclusive lease' do
+ stub_exclusive_lease(lease_key, 'uuid', timeout: lease_timeout)
+
+ service_spy = spy
+
+ allow(rollback_service)
+ .to receive(:new).with(project, project.disk_path, logger: subject.logger)
+ .and_return(service_spy)
+
+ subject.perform(project.id)
+
+ expect(service_spy).to have_received(:execute)
+ end
+
+ it 'skips when it cant acquire the exclusive lease' do
+ stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
+
+ expect(rollback_service).not_to receive(:new)
+
+ subject.perform(project.id)
+ end
+ end
+end
diff --git a/spec/workers/hashed_storage/rollbacker_worker_spec.rb b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
new file mode 100644
index 00000000000..4055f380978
--- /dev/null
+++ b/spec/workers/hashed_storage/rollbacker_worker_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe HashedStorage::RollbackerWorker do
+ subject(:worker) { described_class.new }
+ let(:projects) { create_list(:project, 2, :empty_repo) }
+ let(:ids) { projects.map(&:id) }
+
+ describe '#perform' do
+ it 'delegates to MigratorService' do
+ expect_any_instance_of(Gitlab::HashedStorage::Migrator).to receive(:bulk_rollback).with(start: 5, finish: 10)
+
+ worker.perform(5, 10)
+ end
+
+ it 'rollsback projects in the specified range' do
+ perform_enqueued_jobs do
+ worker.perform(ids.min, ids.max)
+ end
+
+ projects.each do |project|
+ expect(project.reload.legacy_storage?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/workers/invalid_gpg_signature_update_worker_spec.rb b/spec/workers/invalid_gpg_signature_update_worker_spec.rb
index 5972696515b..4f727469ea8 100644
--- a/spec/workers/invalid_gpg_signature_update_worker_spec.rb
+++ b/spec/workers/invalid_gpg_signature_update_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe InvalidGpgSignatureUpdateWorker do
diff --git a/spec/workers/issue_due_scheduler_worker_spec.rb b/spec/workers/issue_due_scheduler_worker_spec.rb
index 2710267d384..61ad8330840 100644
--- a/spec/workers/issue_due_scheduler_worker_spec.rb
+++ b/spec/workers/issue_due_scheduler_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe IssueDueSchedulerWorker do
diff --git a/spec/workers/mail_scheduler/issue_due_worker_spec.rb b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
index 1026ae5b4bf..fa17775e9f2 100644
--- a/spec/workers/mail_scheduler/issue_due_worker_spec.rb
+++ b/spec/workers/mail_scheduler/issue_due_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MailScheduler::IssueDueWorker do
diff --git a/spec/workers/mail_scheduler/notification_service_worker_spec.rb b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
index 5cfba01850c..0729c5f9ffb 100644
--- a/spec/workers/mail_scheduler/notification_service_worker_spec.rb
+++ b/spec/workers/mail_scheduler/notification_service_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MailScheduler::NotificationServiceWorker do
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index b57c275c770..138a99abde6 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe MergeWorker do
diff --git a/spec/workers/migrate_external_diffs_worker_spec.rb b/spec/workers/migrate_external_diffs_worker_spec.rb
new file mode 100644
index 00000000000..88d48cad14b
--- /dev/null
+++ b/spec/workers/migrate_external_diffs_worker_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MigrateExternalDiffsWorker do
+ let(:worker) { described_class.new }
+ let(:diff) { create(:merge_request).merge_request_diff }
+
+ describe '#perform' do
+ it 'migrates the listed diff' do
+ expect_next_instance_of(MergeRequests::MigrateExternalDiffsService) do |instance|
+ expect(instance.diff).to eq(diff)
+ expect(instance).to receive(:execute)
+ end
+
+ worker.perform(diff.id)
+ end
+
+ it 'does nothing if the diff is missing' do
+ diff.destroy
+
+ worker.perform(diff.id)
+ end
+ end
+end
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index 2f21a1321e1..4fbda37e268 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NamespacelessProjectDestroyWorker do
diff --git a/spec/workers/new_issue_worker_spec.rb b/spec/workers/new_issue_worker_spec.rb
index baa8ddb59e5..88a75ce5b70 100644
--- a/spec/workers/new_issue_worker_spec.rb
+++ b/spec/workers/new_issue_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NewIssueWorker do
diff --git a/spec/workers/new_merge_request_worker_spec.rb b/spec/workers/new_merge_request_worker_spec.rb
index c3f29a40d58..d078ddd07d9 100644
--- a/spec/workers/new_merge_request_worker_spec.rb
+++ b/spec/workers/new_merge_request_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe NewMergeRequestWorker do
diff --git a/spec/workers/new_note_worker_spec.rb b/spec/workers/new_note_worker_spec.rb
index 575361c93d4..2966a201a62 100644
--- a/spec/workers/new_note_worker_spec.rb
+++ b/spec/workers/new_note_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require "spec_helper"
describe NewNoteWorker do
diff --git a/spec/workers/pages_domain_removal_cron_worker_spec.rb b/spec/workers/pages_domain_removal_cron_worker_spec.rb
new file mode 100644
index 00000000000..2408ad54189
--- /dev/null
+++ b/spec/workers/pages_domain_removal_cron_worker_spec.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe PagesDomainRemovalCronWorker do
+ subject(:worker) { described_class.new }
+
+ describe '#perform' do
+ context 'when there is domain which should be removed' do
+ let!(:domain_for_removal) { create(:pages_domain, :should_be_removed) }
+
+ it 'removes domain' do
+ expect { worker.perform }.to change { PagesDomain.count }.by(-1)
+ expect(PagesDomain.exists?).to eq(false)
+ end
+ end
+
+ context 'where there is a domain which scheduled for removal in the future' do
+ let!(:domain_for_removal) { create(:pages_domain, :scheduled_for_removal) }
+
+ it 'does not remove pages domain' do
+ expect { worker.perform }.not_to change { PagesDomain.count }
+ expect(PagesDomain.find_by(domain: domain_for_removal.domain)).to be_present
+ end
+ end
+ end
+end
diff --git a/spec/workers/pages_domain_verification_cron_worker_spec.rb b/spec/workers/pages_domain_verification_cron_worker_spec.rb
index 8f780428c82..3fb86adee11 100644
--- a/spec/workers/pages_domain_verification_cron_worker_spec.rb
+++ b/spec/workers/pages_domain_verification_cron_worker_spec.rb
@@ -1,14 +1,23 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PagesDomainVerificationCronWorker do
subject(:worker) { described_class.new }
describe '#perform' do
- it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do
- verified = create(:pages_domain)
- reverify = create(:pages_domain, :reverify)
- disabled = create(:pages_domain, :disabled)
+ let!(:verified) { create(:pages_domain) }
+ let!(:reverify) { create(:pages_domain, :reverify) }
+ let!(:disabled) { create(:pages_domain, :disabled) }
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ expect(PagesDomainVerificationWorker).not_to receive(:perform_async).with(reverify.id)
+
+ worker.perform
+ end
+
+ it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do
[reverify, disabled].each do |domain|
expect(PagesDomainVerificationWorker).to receive(:perform_async).with(domain.id)
end
diff --git a/spec/workers/pages_domain_verification_worker_spec.rb b/spec/workers/pages_domain_verification_worker_spec.rb
index 372fc95ab4a..f51ac1f4323 100644
--- a/spec/workers/pages_domain_verification_worker_spec.rb
+++ b/spec/workers/pages_domain_verification_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PagesDomainVerificationWorker do
@@ -6,6 +8,13 @@ describe PagesDomainVerificationWorker do
let(:domain) { create(:pages_domain) }
describe '#perform' do
+ it 'does nothing if the database is read-only' do
+ allow(Gitlab::Database).to receive(:read_only?).and_return(true)
+ expect(PagesDomain).not_to receive(:find_by).with(id: domain.id)
+
+ worker.perform(domain.id)
+ end
+
it 'does nothing for a non-existent domain' do
domain.destroy
diff --git a/spec/workers/pipeline_hooks_worker_spec.rb b/spec/workers/pipeline_hooks_worker_spec.rb
index 035e329839f..60df08f40da 100644
--- a/spec/workers/pipeline_hooks_worker_spec.rb
+++ b/spec/workers/pipeline_hooks_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineHooksWorker do
diff --git a/spec/workers/pipeline_metrics_worker_spec.rb b/spec/workers/pipeline_metrics_worker_spec.rb
index 896f9e6e7f2..6beecbcd114 100644
--- a/spec/workers/pipeline_metrics_worker_spec.rb
+++ b/spec/workers/pipeline_metrics_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineMetricsWorker do
diff --git a/spec/workers/pipeline_notification_worker_spec.rb b/spec/workers/pipeline_notification_worker_spec.rb
index eb539ffd893..98b0f139fe2 100644
--- a/spec/workers/pipeline_notification_worker_spec.rb
+++ b/spec/workers/pipeline_notification_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineNotificationWorker, :mailer do
diff --git a/spec/workers/pipeline_process_worker_spec.rb b/spec/workers/pipeline_process_worker_spec.rb
index 86e9d7f6684..d33cf72e51e 100644
--- a/spec/workers/pipeline_process_worker_spec.rb
+++ b/spec/workers/pipeline_process_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineProcessWorker do
diff --git a/spec/workers/pipeline_schedule_worker_spec.rb b/spec/workers/pipeline_schedule_worker_spec.rb
index ff408427926..9326db34209 100644
--- a/spec/workers/pipeline_schedule_worker_spec.rb
+++ b/spec/workers/pipeline_schedule_worker_spec.rb
@@ -1,6 +1,10 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineScheduleWorker do
+ include ExclusiveLeaseHelpers
+
subject { described_class.new.perform }
set(:project) { create(:project, :repository) }
@@ -63,47 +67,19 @@ describe PipelineScheduleWorker do
stub_ci_pipeline_yaml_file(YAML.dump(rspec: { variables: 'rspec' } ))
end
- it 'creates a failed pipeline with the reason' do
- expect { subject }.to change { project.ci_pipelines.count }.by(1)
- expect(Ci::Pipeline.last).to be_config_error
- expect(Ci::Pipeline.last.yaml_errors).not_to be_nil
+ it 'does not creates a new pipeline' do
+ expect { subject }.not_to change { project.ci_pipelines.count }
end
end
end
context 'when the schedule is not runnable by the user' do
- before do
- expect(Gitlab::Sentry)
- .to receive(:track_exception)
- .with(Ci::CreatePipelineService::CreateError,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: pipeline_schedule.id } ).once
- end
-
it 'does not deactivate the schedule' do
subject
expect(pipeline_schedule.reload.active).to be_truthy
end
- it 'increments Prometheus counter' do
- expect(Gitlab::Metrics)
- .to receive(:counter)
- .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation")
- .and_call_original
-
- subject
- end
-
- it 'logging a pipeline error' do
- expect(Rails.logger)
- .to receive(:error)
- .with(a_string_matching("Insufficient permissions to create a new pipeline"))
- .and_call_original
-
- subject
- end
-
it 'does not create a pipeline' do
expect { subject }.not_to change { project.ci_pipelines.count }
end
@@ -117,21 +93,6 @@ describe PipelineScheduleWorker do
before do
stub_ci_pipeline_yaml_file(nil)
project.add_maintainer(user)
-
- expect(Gitlab::Sentry)
- .to receive(:track_exception)
- .with(Ci::CreatePipelineService::CreateError,
- issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
- extra: { schedule_id: pipeline_schedule.id } ).once
- end
-
- it 'logging a pipeline error' do
- expect(Rails.logger)
- .to receive(:error)
- .with(a_string_matching("Missing .gitlab-ci.yml file"))
- .and_call_original
-
- subject
end
it 'does not create a pipeline' do
diff --git a/spec/workers/pipeline_success_worker_spec.rb b/spec/workers/pipeline_success_worker_spec.rb
deleted file mode 100644
index d1c84adda6f..00000000000
--- a/spec/workers/pipeline_success_worker_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'spec_helper'
-
-describe PipelineSuccessWorker do
- describe '#perform' do
- context 'when pipeline exists' do
- let(:pipeline) { create(:ci_pipeline, status: 'success') }
-
- it 'performs "merge when pipeline succeeds"' do
- expect_any_instance_of(
- MergeRequests::MergeWhenPipelineSucceedsService
- ).to receive(:trigger)
-
- described_class.new.perform(pipeline.id)
- end
- end
-
- context 'when pipeline does not exist' do
- it 'does not raise exception' do
- expect { described_class.new.perform(123) }
- .not_to raise_error
- end
- end
- end
-end
diff --git a/spec/workers/pipeline_update_worker_spec.rb b/spec/workers/pipeline_update_worker_spec.rb
index 0b456cfd0da..0225e4a9601 100644
--- a/spec/workers/pipeline_update_worker_spec.rb
+++ b/spec/workers/pipeline_update_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PipelineUpdateWorker do
diff --git a/spec/workers/plugin_worker_spec.rb b/spec/workers/plugin_worker_spec.rb
index 9238a8199bc..ca6c9986131 100644
--- a/spec/workers/plugin_worker_spec.rb
+++ b/spec/workers/plugin_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PluginWorker do
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index caae46a3175..39f1beb4efa 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PostReceive do
@@ -33,8 +35,8 @@ describe PostReceive do
describe "#process_project_changes" do
context 'empty changes' do
it "does not call any PushService but runs after project hooks" do
- expect(GitPushService).not_to receive(:new)
- expect(GitTagPushService).not_to receive(:new)
+ expect(Git::BranchPushService).not_to receive(:new)
+ expect(Git::TagPushService).not_to receive(:new)
expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) }
described_class.new.perform(gl_repository, key_id, "")
@@ -45,8 +47,8 @@ describe PostReceive do
let!(:key_id) { "" }
it 'returns false' do
- expect(GitPushService).not_to receive(:new)
- expect(GitTagPushService).not_to receive(:new)
+ expect(Git::BranchPushService).not_to receive(:new)
+ expect(Git::TagPushService).not_to receive(:new)
expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false
end
@@ -60,9 +62,13 @@ describe PostReceive do
context "branches" do
let(:changes) { "123456 789012 refs/heads/tést" }
- it "calls GitPushService" do
- expect_any_instance_of(GitPushService).to receive(:execute).and_return(true)
- expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ it "calls Git::BranchPushService" do
+ expect_next_instance_of(Git::BranchPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
+
+ expect(Git::TagPushService).not_to receive(:new)
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
@@ -70,9 +76,13 @@ describe PostReceive do
context "tags" do
let(:changes) { "123456 789012 refs/tags/tag" }
- it "calls GitTagPushService" do
- expect_any_instance_of(GitPushService).not_to receive(:execute)
- expect_any_instance_of(GitTagPushService).to receive(:execute).and_return(true)
+ it "calls Git::TagPushService" do
+ expect(Git::BranchPushService).not_to receive(:execute)
+
+ expect_next_instance_of(Git::TagPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
@@ -81,18 +91,29 @@ describe PostReceive do
let(:changes) { "123456 789012 refs/merge-requests/123" }
it "does not call any of the services" do
- expect_any_instance_of(GitPushService).not_to receive(:execute)
- expect_any_instance_of(GitTagPushService).not_to receive(:execute)
+ expect(Git::BranchPushService).not_to receive(:new)
+ expect(Git::TagPushService).not_to receive(:new)
+
described_class.new.perform(gl_repository, key_id, base64_changes)
end
end
context "gitlab-ci.yml" do
- let(:changes) { "123456 789012 refs/heads/feature\n654321 210987 refs/tags/tag" }
+ let(:changes) do
+ <<-EOF.strip_heredoc
+ 123456 789012 refs/heads/feature
+ 654321 210987 refs/tags/tag
+ 123456 789012 refs/heads/feature2
+ 123458 789013 refs/heads/feature3
+ 123459 789015 refs/heads/feature4
+ EOF
+ end
+
+ let(:changes_count) { changes.lines.count }
subject { described_class.new.perform(gl_repository, key_id, base64_changes) }
- context "creates a Ci::Pipeline for every change" do
+ context "with valid .gitlab-ci.yml" do
before do
stub_ci_pipeline_to_return_yaml_file
@@ -105,7 +126,33 @@ describe PostReceive do
.and_return(true)
end
- it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
+ context 'when git_push_create_all_pipelines is disabled' do
+ before do
+ stub_feature_flags(git_push_create_all_pipelines: false)
+ end
+
+ it "creates pipeline for branches and tags" do
+ subject
+
+ expect(Ci::Pipeline.pluck(:ref)).to contain_exactly("feature", "tag", "feature2", "feature3")
+ end
+
+ it "creates exactly #{described_class::PIPELINE_PROCESS_LIMIT} pipelines" do
+ expect(changes_count).to be > described_class::PIPELINE_PROCESS_LIMIT
+
+ expect { subject }.to change { Ci::Pipeline.count }.by(described_class::PIPELINE_PROCESS_LIMIT)
+ end
+ end
+
+ context 'when git_push_create_all_pipelines is enabled' do
+ before do
+ stub_feature_flags(git_push_create_all_pipelines: true)
+ end
+
+ it "creates all pipelines" do
+ expect { subject }.to change { Ci::Pipeline.count }.by(changes_count)
+ end
+ end
end
context "does not create a Ci::Pipeline" do
@@ -125,7 +172,9 @@ describe PostReceive do
allow_any_instance_of(Gitlab::DataBuilder::Repository).to receive(:update).and_return(fake_hook_data)
# silence hooks so we can isolate
allow_any_instance_of(Key).to receive(:post_create_hook).and_return(true)
- allow_any_instance_of(GitPushService).to receive(:execute).and_return(true)
+ expect_next_instance_of(Git::BranchPushService) do |service|
+ expect(service).to receive(:execute).and_return(true)
+ end
end
it 'calls SystemHooksService' do
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 2d071c181c2..47bac63511e 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProcessCommitWorker do
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index b9b5445562f..51afb076da1 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectCacheWorker do
@@ -5,9 +7,9 @@ describe ProjectCacheWorker do
let(:worker) { described_class.new }
let(:project) { create(:project, :repository) }
- let(:statistics) { project.statistics }
- let(:lease_key) { "project_cache_worker:#{project.id}:update_statistics" }
+ let(:lease_key) { ["project_cache_worker", project.id, *statistics.sort].join(":") }
let(:lease_timeout) { ProjectCacheWorker::LEASE_TIMEOUT }
+ let(:statistics) { [] }
describe '#perform' do
before do
@@ -23,24 +25,17 @@ describe ProjectCacheWorker do
end
context 'with an existing project without a repository' do
- it 'does nothing' do
+ it 'updates statistics but does not refresh the method cashes' do
allow_any_instance_of(Repository).to receive(:exists?).and_return(false)
- expect(worker).not_to receive(:update_statistics)
+ expect(worker).to receive(:update_statistics)
+ expect_any_instance_of(Repository).not_to receive(:refresh_method_caches)
worker.perform(project.id)
end
end
context 'with an existing project' do
- it 'updates the project statistics' do
- expect(worker).to receive(:update_statistics)
- .with(kind_of(Project), %i(repository_size))
- .and_call_original
-
- worker.perform(project.id, [], %w(repository_size))
- end
-
it 'refreshes the method caches' do
expect_any_instance_of(Repository).to receive(:refresh_method_caches)
.with(%i(readme))
@@ -49,6 +44,18 @@ describe ProjectCacheWorker do
worker.perform(project.id, %w(readme))
end
+ context 'with statistics' do
+ let(:statistics) { %w(repository_size) }
+
+ it 'updates the project statistics' do
+ expect(worker).to receive(:update_statistics)
+ .with(kind_of(Project), statistics)
+ .and_call_original
+
+ worker.perform(project.id, [], statistics)
+ end
+ end
+
context 'with plain readme' do
it 'refreshes the method caches' do
allow(MarkupHelper).to receive(:gitlab_markdown?).and_return(false)
@@ -64,25 +71,34 @@ describe ProjectCacheWorker do
end
describe '#update_statistics' do
+ let(:statistics) { %w(repository_size) }
+
context 'when a lease could not be obtained' do
- it 'does not update the repository size' do
+ it 'does not update the project statistics' do
stub_exclusive_lease_taken(lease_key, timeout: lease_timeout)
- expect(statistics).not_to receive(:refresh!)
+ expect(Projects::UpdateStatisticsService).not_to receive(:new)
+
+ expect(UpdateProjectStatisticsWorker).not_to receive(:perform_in)
- worker.update_statistics(project)
+ worker.update_statistics(project, statistics)
end
end
context 'when a lease could be obtained' do
- it 'updates the project statistics' do
+ it 'updates the project statistics twice' do
stub_exclusive_lease(lease_key, timeout: lease_timeout)
- expect(statistics).to receive(:refresh!)
- .with(only: %i(repository_size))
+ expect(Projects::UpdateStatisticsService).to receive(:new)
+ .with(project, nil, statistics: statistics)
+ .and_call_original
+ .twice
+
+ expect(UpdateProjectStatisticsWorker).to receive(:perform_in)
+ .with(lease_timeout, project.id, statistics)
.and_call_original
- worker.update_statistics(project, %i(repository_size))
+ worker.update_statistics(project, statistics)
end
end
end
diff --git a/spec/workers/project_daily_statistics_worker_spec.rb b/spec/workers/project_daily_statistics_worker_spec.rb
new file mode 100644
index 00000000000..8640add99e5
--- /dev/null
+++ b/spec/workers/project_daily_statistics_worker_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe ProjectDailyStatisticsWorker, '#perform' do
+ let(:worker) { described_class.new }
+ let(:project) { create(:project) }
+
+ describe '#perform' do
+ context 'with a non-existing project' do
+ it 'does nothing' do
+ expect(Projects::FetchStatisticsIncrementService).not_to receive(:new)
+
+ worker.perform(-1)
+ end
+ end
+
+ context 'with an existing project without a repository' do
+ it 'does nothing' do
+ expect(Projects::FetchStatisticsIncrementService).not_to receive(:new)
+
+ worker.perform(project.id)
+ end
+ end
+
+ it 'calls daily_statistics_service with the given project' do
+ project = create(:project, :repository)
+
+ expect_next_instance_of(Projects::FetchStatisticsIncrementService, project) do |service|
+ expect(service).to receive(:execute)
+ end
+
+ worker.perform(project.id)
+ end
+ end
+end
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 6132f145f8d..ec40900a5b7 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectDestroyWorker do
diff --git a/spec/workers/project_export_worker_spec.rb b/spec/workers/project_export_worker_spec.rb
index 8899969c178..8065087796c 100644
--- a/spec/workers/project_export_worker_spec.rb
+++ b/spec/workers/project_export_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ProjectExportWorker do
diff --git a/spec/workers/propagate_service_template_worker_spec.rb b/spec/workers/propagate_service_template_worker_spec.rb
index af1fb80a51d..fb4ced77832 100644
--- a/spec/workers/propagate_service_template_worker_spec.rb
+++ b/spec/workers/propagate_service_template_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PropagateServiceTemplateWorker do
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
index ea2b6ae229e..f1eef1923bf 100644
--- a/spec/workers/prune_old_events_worker_spec.rb
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PruneOldEventsWorker do
diff --git a/spec/workers/prune_web_hook_logs_worker_spec.rb b/spec/workers/prune_web_hook_logs_worker_spec.rb
index b3ec71d4a00..e57334967fd 100644
--- a/spec/workers/prune_web_hook_logs_worker_spec.rb
+++ b/spec/workers/prune_web_hook_logs_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe PruneWebHookLogsWorker do
diff --git a/spec/workers/reactive_caching_worker_spec.rb b/spec/workers/reactive_caching_worker_spec.rb
index 3da851de067..2395e6ec947 100644
--- a/spec/workers/reactive_caching_worker_spec.rb
+++ b/spec/workers/reactive_caching_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe ReactiveCachingWorker do
diff --git a/spec/workers/rebase_worker_spec.rb b/spec/workers/rebase_worker_spec.rb
index 900332ed6b3..578b8cf7451 100644
--- a/spec/workers/rebase_worker_spec.rb
+++ b/spec/workers/rebase_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RebaseWorker, '#perform' do
diff --git a/spec/workers/remote_mirror_notification_worker_spec.rb b/spec/workers/remote_mirror_notification_worker_spec.rb
index e3db10ed645..5182f67b4af 100644
--- a/spec/workers/remote_mirror_notification_worker_spec.rb
+++ b/spec/workers/remote_mirror_notification_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RemoteMirrorNotificationWorker, :mailer do
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
index 689bc3d27b4..10d9aa37dee 100644
--- a/spec/workers/remove_expired_group_links_worker_spec.rb
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RemoveExpiredGroupLinksWorker do
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
index 058fdf4c009..69a5725bb35 100644
--- a/spec/workers/remove_expired_members_worker_spec.rb
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RemoveExpiredMembersWorker do
diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
index 57f83c1dbe9..0e21933a9a5 100644
--- a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
+++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RemoveUnreferencedLfsObjectsWorker do
diff --git a/spec/workers/repository_check/batch_worker_spec.rb b/spec/workers/repository_check/batch_worker_spec.rb
index 50b93fce2dc..051c6a5d141 100644
--- a/spec/workers/repository_check/batch_worker_spec.rb
+++ b/spec/workers/repository_check/batch_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryCheck::BatchWorker do
diff --git a/spec/workers/repository_check/clear_worker_spec.rb b/spec/workers/repository_check/clear_worker_spec.rb
index 1c49415d46c..7ad9e287204 100644
--- a/spec/workers/repository_check/clear_worker_spec.rb
+++ b/spec/workers/repository_check/clear_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryCheck::ClearWorker do
diff --git a/spec/workers/repository_check/dispatch_worker_spec.rb b/spec/workers/repository_check/dispatch_worker_spec.rb
index 7877429aa8f..03efb6a0a80 100644
--- a/spec/workers/repository_check/dispatch_worker_spec.rb
+++ b/spec/workers/repository_check/dispatch_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryCheck::DispatchWorker do
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index f11875cffd1..65e1c5e9d5d 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
require 'fileutils'
diff --git a/spec/workers/repository_cleanup_worker_spec.rb b/spec/workers/repository_cleanup_worker_spec.rb
index 3adae0b6cfa..e58664cf22a 100644
--- a/spec/workers/repository_cleanup_worker_spec.rb
+++ b/spec/workers/repository_cleanup_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryCleanupWorker do
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 31bfe88d0bd..26fd67adfaa 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryForkWorker do
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 87ac4bc05c1..b8767af8eee 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RepositoryImportWorker do
diff --git a/spec/workers/repository_remove_remote_worker_spec.rb b/spec/workers/repository_remove_remote_worker_spec.rb
index 6ddb653d142..6eba5c50960 100644
--- a/spec/workers/repository_remove_remote_worker_spec.rb
+++ b/spec/workers/repository_remove_remote_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe RepositoryRemoveRemoteWorker do
diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb
index b582a3650b6..4de51ecb3e9 100644
--- a/spec/workers/repository_update_remote_mirror_worker_spec.rb
+++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe RepositoryUpdateRemoteMirrorWorker do
diff --git a/spec/workers/run_pipeline_schedule_worker_spec.rb b/spec/workers/run_pipeline_schedule_worker_spec.rb
index 481a84837f9..7414470f8e7 100644
--- a/spec/workers/run_pipeline_schedule_worker_spec.rb
+++ b/spec/workers/run_pipeline_schedule_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe RunPipelineScheduleWorker do
@@ -30,7 +32,37 @@ describe RunPipelineScheduleWorker do
it 'calls the Service' do
expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: pipeline_schedule.ref).and_return(create_pipeline_service)
- expect(create_pipeline_service).to receive(:execute).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule)
+ expect(create_pipeline_service).to receive(:execute!).with(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: pipeline_schedule)
+
+ worker.perform(pipeline_schedule.id, user.id)
+ end
+ end
+
+ context 'when database statement timeout happens' do
+ before do
+ allow(Ci::CreatePipelineService).to receive(:new) { raise ActiveRecord::StatementInvalid }
+
+ expect(Gitlab::Sentry)
+ .to receive(:track_exception)
+ .with(ActiveRecord::StatementInvalid,
+ issue_url: 'https://gitlab.com/gitlab-org/gitlab-ce/issues/41231',
+ extra: { schedule_id: pipeline_schedule.id } ).once
+ end
+
+ it 'increments Prometheus counter' do
+ expect(Gitlab::Metrics)
+ .to receive(:counter)
+ .with(:pipeline_schedule_creation_failed_total, "Counter of failed attempts of pipeline schedule creation")
+ .and_call_original
+
+ worker.perform(pipeline_schedule.id, user.id)
+ end
+
+ it 'logging a pipeline error' do
+ expect(Rails.logger)
+ .to receive(:error)
+ .with(a_string_matching('ActiveRecord::StatementInvalid'))
+ .and_call_original
worker.perform(pipeline_schedule.id, user.id)
end
diff --git a/spec/workers/schedule_migrate_external_diffs_worker_spec.rb b/spec/workers/schedule_migrate_external_diffs_worker_spec.rb
new file mode 100644
index 00000000000..9d6fecc9f4e
--- /dev/null
+++ b/spec/workers/schedule_migrate_external_diffs_worker_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ScheduleMigrateExternalDiffsWorker do
+ include ExclusiveLeaseHelpers
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ it 'triggers a scan for diffs to migrate' do
+ expect(MergeRequests::MigrateExternalDiffsService).to receive(:enqueue!)
+
+ worker.perform
+ end
+
+ it 'will not run if the lease is already taken' do
+ stub_exclusive_lease_taken('schedule_migrate_external_diffs_worker', timeout: 2.hours)
+
+ expect(MergeRequests::MigrateExternalDiffsService).not_to receive(:enqueue!)
+
+ worker.perform
+ end
+ end
+end
diff --git a/spec/workers/stage_update_worker_spec.rb b/spec/workers/stage_update_worker_spec.rb
index 7bc76c79464..429d42bac29 100644
--- a/spec/workers/stage_update_worker_spec.rb
+++ b/spec/workers/stage_update_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe StageUpdateWorker do
diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb
index e09b8e5b964..72de62f1188 100644
--- a/spec/workers/stuck_ci_jobs_worker_spec.rb
+++ b/spec/workers/stuck_ci_jobs_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe StuckCiJobsWorker do
diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb
index e94d2be9850..dcb8e59ed28 100644
--- a/spec/workers/stuck_import_jobs_worker_spec.rb
+++ b/spec/workers/stuck_import_jobs_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe StuckImportJobsWorker do
diff --git a/spec/workers/stuck_merge_jobs_worker_spec.rb b/spec/workers/stuck_merge_jobs_worker_spec.rb
index 5aaff27a6b2..09efed6d2cf 100644
--- a/spec/workers/stuck_merge_jobs_worker_spec.rb
+++ b/spec/workers/stuck_merge_jobs_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe StuckMergeJobsWorker do
diff --git a/spec/workers/system_hook_push_worker_spec.rb b/spec/workers/system_hook_push_worker_spec.rb
index b1d446ed25f..890a622d11a 100644
--- a/spec/workers/system_hook_push_worker_spec.rb
+++ b/spec/workers/system_hook_push_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe SystemHookPushWorker do
diff --git a/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb b/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
index 9d7c0b8f560..0907e2768ba 100644
--- a/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
+++ b/spec/workers/todos_destroyer/confidential_issue_worker_spec.rb
@@ -1,12 +1,21 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TodosDestroyer::ConfidentialIssueWorker do
- it "calls the Todos::Destroy::ConfidentialIssueService with the params it was given" do
- service = double
+ let(:service) { double }
- expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(100).and_return(service)
+ it "calls the Todos::Destroy::ConfidentialIssueService with issue_id parameter" do
+ expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(issue_id: 100, project_id: nil).and_return(service)
expect(service).to receive(:execute)
described_class.new.perform(100)
end
+
+ it "calls the Todos::Destroy::ConfidentialIssueService with project_id parameter" do
+ expect(::Todos::Destroy::ConfidentialIssueService).to receive(:new).with(issue_id: nil, project_id: 100).and_return(service)
+ expect(service).to receive(:execute)
+
+ described_class.new.perform(nil, 100)
+ end
end
diff --git a/spec/workers/todos_destroyer/entity_leave_worker_spec.rb b/spec/workers/todos_destroyer/entity_leave_worker_spec.rb
index 955447906aa..cb14fac0910 100644
--- a/spec/workers/todos_destroyer/entity_leave_worker_spec.rb
+++ b/spec/workers/todos_destroyer/entity_leave_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TodosDestroyer::EntityLeaveWorker do
diff --git a/spec/workers/todos_destroyer/group_private_worker_spec.rb b/spec/workers/todos_destroyer/group_private_worker_spec.rb
index fcc38989ced..d9a240136d5 100644
--- a/spec/workers/todos_destroyer/group_private_worker_spec.rb
+++ b/spec/workers/todos_destroyer/group_private_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TodosDestroyer::GroupPrivateWorker do
diff --git a/spec/workers/todos_destroyer/private_features_worker_spec.rb b/spec/workers/todos_destroyer/private_features_worker_spec.rb
index 9599f5ee071..abd04acc3bd 100644
--- a/spec/workers/todos_destroyer/private_features_worker_spec.rb
+++ b/spec/workers/todos_destroyer/private_features_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TodosDestroyer::PrivateFeaturesWorker do
diff --git a/spec/workers/todos_destroyer/project_private_worker_spec.rb b/spec/workers/todos_destroyer/project_private_worker_spec.rb
index 15d926fa9d5..c1bb0438ec3 100644
--- a/spec/workers/todos_destroyer/project_private_worker_spec.rb
+++ b/spec/workers/todos_destroyer/project_private_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TodosDestroyer::ProjectPrivateWorker do
diff --git a/spec/workers/trending_projects_worker_spec.rb b/spec/workers/trending_projects_worker_spec.rb
index c3c6fdcf2d5..6e524085662 100644
--- a/spec/workers/trending_projects_worker_spec.rb
+++ b/spec/workers/trending_projects_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe TrendingProjectsWorker do
diff --git a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
index 963237ceadf..c4af829a5e2 100644
--- a/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
+++ b/spec/workers/update_head_pipeline_for_merge_request_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UpdateHeadPipelineForMergeRequestWorker do
@@ -18,7 +20,7 @@ describe UpdateHeadPipelineForMergeRequestWorker do
context 'when merge request sha does not equal pipeline sha' do
before do
- merge_request.merge_request_diff.update(head_commit_sha: 'different_sha')
+ merge_request.merge_request_diff.update(head_commit_sha: Digest::SHA1.hexdigest(SecureRandom.hex))
end
it 'does not update head pipeline' do
@@ -39,7 +41,7 @@ describe UpdateHeadPipelineForMergeRequestWorker do
let!(:merge_request_pipeline) do
create(:ci_pipeline,
project: project,
- source: :merge_request,
+ source: :merge_request_event,
sha: latest_sha,
merge_request: merge_request)
end
diff --git a/spec/workers/update_merge_requests_worker_spec.rb b/spec/workers/update_merge_requests_worker_spec.rb
index 0b553db0ca4..486dade454a 100644
--- a/spec/workers/update_merge_requests_worker_spec.rb
+++ b/spec/workers/update_merge_requests_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe UpdateMergeRequestsWorker do
diff --git a/spec/workers/update_project_statistics_worker_spec.rb b/spec/workers/update_project_statistics_worker_spec.rb
new file mode 100644
index 00000000000..a268fd2e4ba
--- /dev/null
+++ b/spec/workers/update_project_statistics_worker_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe UpdateProjectStatisticsWorker do
+ let(:worker) { described_class.new }
+ let(:project) { create(:project, :repository) }
+ let(:statistics) { %w(repository_size) }
+
+ describe '#perform' do
+ it 'updates the project statistics' do
+ expect(Projects::UpdateStatisticsService).to receive(:new)
+ .with(project, nil, statistics: statistics)
+ .and_call_original
+
+ worker.perform(project.id, statistics)
+ end
+ end
+end
diff --git a/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb
index 9e50ce15871..7202c8001b4 100644
--- a/spec/workers/upload_checksum_worker_spec.rb
+++ b/spec/workers/upload_checksum_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'rails_helper'
describe UploadChecksumWorker do
diff --git a/spec/workers/wait_for_cluster_creation_worker_spec.rb b/spec/workers/wait_for_cluster_creation_worker_spec.rb
index 0e92b298178..850eba263a7 100644
--- a/spec/workers/wait_for_cluster_creation_worker_spec.rb
+++ b/spec/workers/wait_for_cluster_creation_worker_spec.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe WaitForClusterCreationWorker do