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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2018-02-06 11:32:18 +0300
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2018-02-06 11:32:18 +0300
commit066d4deaed3d8698a9d1decd138871528cecc4af (patch)
treeaee535d5f19f0798ab155fdb2f65a4c743e54422 /spec
parente011e291dadb00b260e74c25a64134f78d4d2177 (diff)
parent84bda43a3c0ce13a436748d0bc0ea943f6ebccb3 (diff)
Merge branch 'master' into fix/gb/fix-redundant-pipeline-stages
* master: (441 commits) Conflicts: db/schema.rb
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/admin/gitaly_servers_controller_spec.rb15
-rw-r--r--spec/controllers/groups/uploads_controller_spec.rb4
-rw-r--r--spec/controllers/groups_controller_spec.rb107
-rw-r--r--spec/controllers/health_check_controller_spec.rb2
-rw-r--r--spec/controllers/health_controller_spec.rb2
-rw-r--r--spec/controllers/oauth/applications_controller_spec.rb3
-rw-r--r--spec/controllers/projects/artifacts_controller_spec.rb3
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb12
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb4
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb2
-rw-r--r--spec/controllers/projects/todos_controller_spec.rb4
-rw-r--r--spec/controllers/uploads_controller_spec.rb13
-rw-r--r--spec/controllers/user_callouts_controller_spec.rb49
-rw-r--r--spec/factories/commits.rb2
-rw-r--r--spec/factories/deployments.rb3
-rw-r--r--spec/factories/events.rb2
-rw-r--r--spec/factories/groups.rb2
-rw-r--r--spec/factories/issues.rb2
-rw-r--r--spec/factories/keys.rb4
-rw-r--r--spec/factories/merge_requests.rb2
-rw-r--r--spec/factories/notes.rb6
-rw-r--r--spec/factories/project_wikis.rb2
-rw-r--r--spec/factories/projects.rb8
-rw-r--r--spec/factories/sent_notifications.rb2
-rw-r--r--spec/factories/snippets.rb1
-rw-r--r--spec/factories/subscriptions.rb2
-rw-r--r--spec/factories/timelogs.rb2
-rw-r--r--spec/factories/todos.rb4
-rw-r--r--spec/factories/uploads.rb32
-rw-r--r--spec/factories/user_callouts.rb7
-rw-r--r--spec/factories/users.rb2
-rw-r--r--spec/features/admin/admin_health_check_spec.rb4
-rw-r--r--spec/features/admin/admin_runners_spec.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb12
-rw-r--r--spec/features/atom/users_spec.rb4
-rw-r--r--spec/features/commits_spec.rb2
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb22
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb7
-rw-r--r--spec/features/global_search_spec.rb2
-rw-r--r--spec/features/groups/issues_spec.rb69
-rw-r--r--spec/features/issues/spam_issues_spec.rb5
-rw-r--r--spec/features/markdown_spec.rb8
-rw-r--r--spec/features/merge_request/user_awards_emoji_spec.rb2
-rw-r--r--spec/features/merge_request/user_resolves_conflicts_spec.rb10
-rw-r--r--spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb2
-rw-r--r--spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb2
-rw-r--r--spec/features/projects/badges/coverage_spec.rb4
-rw-r--r--spec/features/projects/blobs/edit_spec.rb22
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb2
-rw-r--r--spec/features/projects/clusters/user_spec.rb2
-rw-r--r--spec/features/projects/clusters_spec.rb6
-rw-r--r--spec/features/projects/import_export/namespace_export_file_spec.rb55
-rw-r--r--spec/features/projects/members/share_with_group_spec.rb2
-rw-r--r--spec/features/projects/pipeline_schedules_spec.rb30
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb74
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb3
-rw-r--r--spec/features/user_page_spec.rb107
-rw-r--r--spec/finders/group_projects_finder_spec.rb76
-rw-r--r--spec/finders/issues_finder_spec.rb44
-rw-r--r--spec/finders/merge_requests_finder_spec.rb41
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/basic.json4
-rw-r--r--spec/fixtures/emails/attachment.eml28
-rw-r--r--spec/fixtures/markdown.md.erb12
-rw-r--r--spec/helpers/application_helper_spec.rb6
-rw-r--r--spec/helpers/groups_helper_spec.rb4
-rw-r--r--spec/helpers/labels_helper_spec.rb2
-rw-r--r--spec/helpers/user_callouts_helper_spec.rb47
-rw-r--r--spec/helpers/version_check_helper_spec.rb6
-rw-r--r--spec/initializers/grape_route_helpers_fix_spec.rb14
-rw-r--r--spec/initializers/settings_spec.rb2
-rw-r--r--spec/javascripts/api_spec.js177
-rw-r--r--spec/javascripts/behaviors/secret_values_spec.js110
-rw-r--r--spec/javascripts/blob/viewer/index_spec.js36
-rw-r--r--spec/javascripts/ci_variable_list/ci_variable_list_spec.js163
-rw-r--r--spec/javascripts/ci_variable_list/native_form_variable_list_spec.js30
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js24
-rw-r--r--spec/javascripts/clusters/clusters_index_spec.js58
-rw-r--r--spec/javascripts/collapsed_sidebar_todo_spec.js139
-rw-r--r--spec/javascripts/commit/commit_pipeline_status_component_spec.js104
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js5
-rw-r--r--spec/javascripts/commits_spec.js43
-rw-r--r--spec/javascripts/create_item_dropdown_spec.js139
-rw-r--r--spec/javascripts/environments/environment_item_spec.js4
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_helper_spec.js231
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_options_spec.js30
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_spec.js131
-rw-r--r--spec/javascripts/fixtures/clusters.rb15
-rw-r--r--spec/javascripts/fixtures/pipeline_schedules.rb43
-rw-r--r--spec/javascripts/fixtures/projects.json2
-rw-r--r--spec/javascripts/gfm_auto_complete_spec.js13
-rw-r--r--spec/javascripts/helpers/user_mock_data_helper.js2
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js128
-rw-r--r--spec/javascripts/issuable_spec.js26
-rw-r--r--spec/javascripts/issue_spec.js144
-rw-r--r--spec/javascripts/job_spec.js261
-rw-r--r--spec/javascripts/jobs/mock_data.js8
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js43
-rw-r--r--spec/javascripts/lib/utils/ajax_cache_spec.js68
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js51
-rw-r--r--spec/javascripts/lib/utils/users_cache_spec.js4
-rw-r--r--spec/javascripts/merge_request_spec.js28
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js96
-rw-r--r--spec/javascripts/mini_pipeline_graph_dropdown_spec.js73
-rw-r--r--spec/javascripts/notes/mock_data.js2
-rw-r--r--spec/javascripts/notes_spec.js212
-rw-r--r--spec/javascripts/pager_spec.js81
-rw-r--r--spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js95
-rw-r--r--spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js145
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js7
-rw-r--r--spec/javascripts/pipelines/pipelines_table_spec.js5
-rw-r--r--spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js44
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js6
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js6
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js6
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js32
-rw-r--r--spec/javascripts/right_sidebar_spec.js26
-rw-r--r--spec/javascripts/sidebar/mock_data.js34
-rw-r--r--spec/javascripts/sidebar/sidebar_store_spec.js6
-rw-r--r--spec/javascripts/test_bundle.js10
-rw-r--r--spec/javascripts/toggle_buttons_spec.js120
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js58
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js79
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js269
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js79
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js131
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js44
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js36
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js106
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js138
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js123
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js33
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js131
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js208
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js34
-rw-r--r--spec/javascripts/vue_mr_widget/mock_data.js10
-rw-r--r--spec/javascripts/vue_shared/components/confirmation_input_spec.js63
-rw-r--r--spec/lib/banzai/color_parser_spec.rb90
-rw-r--r--spec/lib/banzai/filter/color_filter_spec.rb61
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/image_link_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb4
-rw-r--r--spec/lib/file_size_validator_spec.rb2
-rw-r--r--spec/lib/gitaly/server_spec.rb30
-rw-r--r--spec/lib/gitlab/background_migration/deserialize_merge_request_diffs_and_commits_spec.rb2
-rw-r--r--spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb4
-rw-r--r--spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb57
-rw-r--r--spec/lib/gitlab/badge/coverage/template_spec.rb18
-rw-r--r--spec/lib/gitlab/current_settings_spec.rb40
-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/rename_namespaces_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb2
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb2
-rw-r--r--spec/lib/gitlab/gfm/uploads_rewriter_spec.rb2
-rw-r--r--spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb28
-rw-r--r--spec/lib/gitlab/git/attributes_parser_spec.rb (renamed from spec/lib/gitlab/git/attributes_spec.rb)53
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb58
-rw-r--r--spec/lib/gitlab/git/info_attributes_spec.rb43
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb259
-rw-r--r--spec/lib/gitlab/git/tree_spec.rb20
-rw-r--r--spec/lib/gitlab/git/wiki_spec.rb36
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb23
-rw-r--r--spec/lib/gitlab/gitaly_client/health_check_service_spec.rb41
-rw-r--r--spec/lib/gitlab/gitaly_client/operation_service_spec.rb49
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb25
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb6
-rw-r--r--spec/lib/gitlab/health_checks/gitaly_check_spec.rb57
-rw-r--r--spec/lib/gitlab/import_export/project.json2
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb6
-rw-r--r--spec/lib/gitlab/import_export/uploads_restorer_spec.rb9
-rw-r--r--spec/lib/gitlab/import_export/uploads_saver_spec.rb4
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb4
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb24
-rw-r--r--spec/lib/gitlab/ldap/person_spec.rb21
-rw-r--r--spec/lib/gitlab/metrics/method_call_spec.rb44
-rw-r--r--spec/lib/gitlab/metrics/methods_spec.rb137
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb22
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb7
-rw-r--r--spec/lib/gitlab/metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb30
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb2
-rw-r--r--spec/lib/gitlab/popen/runner_spec.rb139
-rw-r--r--spec/lib/gitlab/popen_spec.rb16
-rw-r--r--spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb19
-rw-r--r--spec/lib/gitlab/query_limiting/middleware_spec.rb72
-rw-r--r--spec/lib/gitlab/query_limiting/transaction_spec.rb144
-rw-r--r--spec/lib/gitlab/query_limiting_spec.rb65
-rw-r--r--spec/lib/gitlab/search_results_spec.rb58
-rw-r--r--spec/lib/gitlab/slash_commands/issue_search_spec.rb2
-rw-r--r--spec/lib/gitlab/ssh_public_key_spec.rb35
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb6
-rw-r--r--spec/lib/gitlab/visibility_level_spec.rb9
-rw-r--r--spec/mailers/notify_spec.rb2
-rw-r--r--spec/migrations/add_foreign_keys_to_todos_spec.rb65
-rw-r--r--spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb2
-rw-r--r--spec/migrations/calculate_conv_dev_index_percentages_spec.rb2
-rw-r--r--spec/migrations/fix_wrongly_renamed_routes_spec.rb2
-rw-r--r--spec/migrations/migrate_issues_to_ghost_user_spec.rb6
-rw-r--r--spec/migrations/migrate_user_activities_to_users_last_activity_on_spec.rb2
-rw-r--r--spec/migrations/migrate_user_project_view_spec.rb2
-rw-r--r--spec/migrations/remove_duplicate_mr_events_spec.rb2
-rw-r--r--spec/migrations/remove_project_labels_group_id_spec.rb21
-rw-r--r--spec/migrations/rename_more_reserved_project_names_spec.rb4
-rw-r--r--spec/migrations/rename_reserved_project_names_spec.rb4
-rw-r--r--spec/migrations/rename_users_with_renamed_namespace_spec.rb2
-rw-r--r--spec/migrations/update_retried_for_ci_build_spec.rb2
-rw-r--r--spec/models/ci/build_spec.rb2
-rw-r--r--spec/models/commit_spec.rb6
-rw-r--r--spec/models/concerns/avatarable_spec.rb39
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb10
-rw-r--r--spec/models/group_spec.rb16
-rw-r--r--spec/models/key_spec.rb58
-rw-r--r--spec/models/member_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb50
-rw-r--r--spec/models/namespace_spec.rb68
-rw-r--r--spec/models/note_spec.rb4
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb27
-rw-r--r--spec/models/project_spec.rb49
-rw-r--r--spec/models/project_wiki_spec.rb13
-rw-r--r--spec/models/repository_spec.rb127
-rw-r--r--spec/models/todo_spec.rb1
-rw-r--r--spec/models/upload_spec.rb79
-rw-r--r--spec/models/user_callout_spec.rb16
-rw-r--r--spec/models/user_spec.rb12
-rw-r--r--spec/models/wiki_page_spec.rb141
-rw-r--r--spec/policies/ci/pipeline_schedule_policy_spec.rb14
-rw-r--r--spec/policies/project_policy_spec.rb2
-rw-r--r--spec/requests/api/applications_spec.rb86
-rw-r--r--spec/requests/api/commits_spec.rb12
-rw-r--r--spec/requests/api/groups_spec.rb15
-rw-r--r--spec/requests/api/issues_spec.rb4
-rw-r--r--spec/requests/api/jobs_spec.rb71
-rw-r--r--spec/requests/api/members_spec.rb15
-rw-r--r--spec/requests/api/merge_requests_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/runner_spec.rb4
-rw-r--r--spec/requests/api/v3/builds_spec.rb54
-rw-r--r--spec/requests/api/v3/commits_spec.rb6
-rw-r--r--spec/requests/api/v3/projects_spec.rb2
-rw-r--r--spec/requests/lfs_http_spec.rb4
-rw-r--r--spec/services/groups/transfer_service_spec.rb414
-rw-r--r--spec/services/issues/close_service_spec.rb2
-rw-r--r--spec/services/issues/move_service_spec.rb8
-rw-r--r--spec/services/issues/update_service_spec.rb3
-rw-r--r--spec/services/merge_requests/build_service_spec.rb58
-rw-r--r--spec/services/merge_requests/close_service_spec.rb2
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb3
-rw-r--r--spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb2
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb79
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb2
-rw-r--r--spec/services/merge_requests/update_service_spec.rb3
-rw-r--r--spec/services/notification_service_spec.rb6
-rw-r--r--spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb4
-rw-r--r--spec/services/system_note_service_spec.rb37
-rw-r--r--spec/support/db_cleaner.rb26
-rw-r--r--spec/support/features/discussion_comments_shared_example.rb6
-rw-r--r--spec/support/javascript_fixtures_helpers.rb1
-rw-r--r--spec/support/matchers/markdown_matchers.rb21
-rw-r--r--spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb62
-rw-r--r--spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb48
-rw-r--r--spec/support/stored_repositories.rb4
-rw-r--r--spec/support/stub_env.rb2
-rw-r--r--spec/support/test_env.rb4
-rw-r--r--spec/support/track_untracked_uploads_helpers.rb2
-rw-r--r--spec/support/unique_ip_check_shared_examples.rb6
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/git_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb6
-rw-r--r--spec/tasks/gitlab/task_helpers_spec.rb1
-rw-r--r--spec/uploaders/attachment_uploader_spec.rb30
-rw-r--r--spec/uploaders/avatar_uploader_spec.rb18
-rw-r--r--spec/uploaders/file_mover_spec.rb18
-rw-r--r--spec/uploaders/file_uploader_spec.rb132
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb2
-rw-r--r--spec/uploaders/job_artifact_uploader_spec.rb34
-rw-r--r--spec/uploaders/legacy_artifact_uploader_spec.rb49
-rw-r--r--spec/uploaders/lfs_object_uploader_spec.rb38
-rw-r--r--spec/uploaders/namespace_file_uploader_spec.rb21
-rw-r--r--spec/uploaders/personal_file_uploader_spec.rb28
-rw-r--r--spec/uploaders/records_uploads_spec.rb73
-rw-r--r--spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb47
-rw-r--r--spec/views/projects/pipelines_settings/_show.html.haml_spec.rb8
-rw-r--r--spec/workers/repository_import_worker_spec.rb15
-rw-r--r--spec/workers/upload_checksum_worker_spec.rb29
293 files changed, 7381 insertions, 2906 deletions
diff --git a/spec/controllers/admin/gitaly_servers_controller_spec.rb b/spec/controllers/admin/gitaly_servers_controller_spec.rb
new file mode 100644
index 00000000000..b7378aa37d0
--- /dev/null
+++ b/spec/controllers/admin/gitaly_servers_controller_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe Admin::GitalyServersController do
+ describe '#index' do
+ before do
+ sign_in(create(:admin))
+ end
+
+ it 'shows the gitaly servers page' do
+ get :index
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+end
diff --git a/spec/controllers/groups/uploads_controller_spec.rb b/spec/controllers/groups/uploads_controller_spec.rb
index 67a11e56e94..6a1869d1a48 100644
--- a/spec/controllers/groups/uploads_controller_spec.rb
+++ b/spec/controllers/groups/uploads_controller_spec.rb
@@ -6,5 +6,7 @@ describe Groups::UploadsController do
{ group_id: model }
end
- it_behaves_like 'handle uploads'
+ it_behaves_like 'handle uploads' do
+ let(:uploader_class) { NamespaceFileUploader }
+ end
end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index a9cfd964dd5..8688fb33f0d 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -85,6 +85,30 @@ describe GroupsController do
end
end
+ describe 'GET #activity' do
+ render_views
+
+ before do
+ sign_in(user)
+ project
+ end
+
+ context 'as json' do
+ it 'includes all projects in event feed' do
+ 3.times do
+ project = create(:project, group: group)
+ create(:event, project: project)
+ end
+
+ get :activity, id: group.to_param, format: :json
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['count']).to eq(3)
+ expect(assigns(:projects).limit_value).to be_nil
+ end
+ end
+ end
+
describe 'POST #create' do
context 'when creating subgroups', :nested_groups do
[true, false].each do |can_create_group_status|
@@ -472,4 +496,87 @@ describe GroupsController do
"Group '#{redirect_route.path}' was moved to '#{group.full_path}'. Please update any links and bookmarks that may still have the old path."
end
end
+
+ describe 'PUT transfer', :postgresql do
+ before do
+ sign_in(user)
+ end
+
+ context 'when transfering to a subgroup goes right' do
+ let(:new_parent_group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+ let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) }
+
+ before do
+ put :transfer,
+ id: group.to_param,
+ new_parent_group_id: new_parent_group.id
+ end
+
+ it 'should return a notice' do
+ expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
+ end
+
+ it 'should redirect to the new path' do
+ expect(response).to redirect_to("/#{new_parent_group.path}/#{group.path}")
+ end
+ end
+
+ context 'when converting to a root group goes right' do
+ let(:group) { create(:group, :public, :nested) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+
+ before do
+ put :transfer,
+ id: group.to_param,
+ new_parent_group_id: ''
+ end
+
+ it 'should return a notice' do
+ expect(flash[:notice]).to eq("Group '#{group.name}' was successfully transferred.")
+ end
+
+ it 'should redirect to the new path' do
+ expect(response).to redirect_to("/#{group.path}")
+ end
+ end
+
+ context 'When the transfer goes wrong' do
+ let(:new_parent_group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+ let!(:new_parent_group_member) { create(:group_member, :owner, group: new_parent_group, user: user) }
+
+ before do
+ allow_any_instance_of(::Groups::TransferService).to receive(:proceed_to_transfer).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved')
+
+ put :transfer,
+ id: group.to_param,
+ new_parent_group_id: new_parent_group.id
+ end
+
+ it 'should return 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)
+ end
+ end
+
+ context 'when the user is not allowed to transfer the group' do
+ let(:new_parent_group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, :guest, group: group, user: user) }
+ let!(:new_parent_group_member) { create(:group_member, :guest, group: new_parent_group, user: user) }
+
+ before do
+ put :transfer,
+ id: group.to_param,
+ new_parent_group_id: new_parent_group.id
+ end
+
+ it 'should be denied' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
end
diff --git a/spec/controllers/health_check_controller_spec.rb b/spec/controllers/health_check_controller_spec.rb
index 2cead1770c9..387ca46ef6f 100644
--- a/spec/controllers/health_check_controller_spec.rb
+++ b/spec/controllers/health_check_controller_spec.rb
@@ -5,7 +5,7 @@ describe HealthCheckController do
let(:json_response) { JSON.parse(response.body) }
let(:xml_response) { Hash.from_xml(response.body)['hash'] }
- let(:token) { current_application_settings.health_check_access_token }
+ let(:token) { Gitlab::CurrentSettings.health_check_access_token }
let(:whitelisted_ip) { '127.0.0.1' }
let(:not_whitelisted_ip) { '127.0.0.2' }
diff --git a/spec/controllers/health_controller_spec.rb b/spec/controllers/health_controller_spec.rb
index 95946def5f9..542eddc2d16 100644
--- a/spec/controllers/health_controller_spec.rb
+++ b/spec/controllers/health_controller_spec.rb
@@ -4,7 +4,7 @@ describe HealthController do
include StubENV
let(:json_response) { JSON.parse(response.body) }
- let(:token) { current_application_settings.health_check_access_token }
+ let(:token) { Gitlab::CurrentSettings.health_check_access_token }
let(:whitelisted_ip) { '127.0.0.1' }
let(:not_whitelisted_ip) { '127.0.0.2' }
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index b38652e7ab9..1195f44f37d 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -16,8 +16,7 @@ describe Oauth::ApplicationsController do
end
it 'redirects back to profile page if OAuth applications are disabled' do
- settings = double(user_oauth_applications?: false)
- allow_any_instance_of(Gitlab::CurrentSettings).to receive(:current_application_settings).and_return(settings)
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false)
get :index
diff --git a/spec/controllers/projects/artifacts_controller_spec.rb b/spec/controllers/projects/artifacts_controller_spec.rb
index 12cb7b2647f..25a2e13fe1a 100644
--- a/spec/controllers/projects/artifacts_controller_spec.rb
+++ b/spec/controllers/projects/artifacts_controller_spec.rb
@@ -145,8 +145,7 @@ describe Projects::ArtifactsController do
context 'when using local file storage' do
it_behaves_like 'a valid file' do
let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
- let(:store) { ObjectStoreUploader::LOCAL_STORE }
- let(:archive_path) { JobArtifactUploader.local_store_path }
+ let(:archive_path) { JobArtifactUploader.root }
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 4a2998b4ccd..9656e7f7e74 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -102,6 +102,18 @@ describe Projects::IssuesController do
expect(response).to redirect_to(namespace_project_issues_path(page: last_page, state: controller.params[:state], scope: controller.params[:scope]))
end
+
+ it 'does not use pagination if disabled' do
+ allow(controller).to receive(:pagination_disabled?).and_return(true)
+
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ page: (last_page + 1).to_param
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(assigns(:issues).size).to eq(2)
+ end
end
end
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index e6a4e7c8257..db595430979 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -137,8 +137,8 @@ describe Projects::JobsController do
it 'exposes needed information' do
expect(response).to have_gitlab_http_status(:ok)
- expect(json_response['raw_path']).to match(/jobs\/\d+\/raw\z/)
- expect(json_response.dig('merge_request', 'path')).to match(/merge_requests\/\d+\z/)
+ expect(json_response['raw_path']).to match(%r{jobs/\d+/raw\z})
+ expect(json_response.dig('merge_request', 'path')).to match(%r{merge_requests/\d+\z})
expect(json_response['new_issue_path'])
.to include('/issues/new')
end
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index 3a0c3faa7b4..b7df42168e0 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -53,7 +53,7 @@ describe Projects::RawController do
end
it 'serves the file' do
- expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
+ expect(controller).to receive(:send_file).with("#{LfsObjectUploader.root}/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: 'lfs_object.iso', disposition: 'attachment')
get(:show,
namespace_id: public_project.namespace.to_param,
project_id: public_project,
diff --git a/spec/controllers/projects/todos_controller_spec.rb b/spec/controllers/projects/todos_controller_spec.rb
index e2524be7724..1ce7e84bef9 100644
--- a/spec/controllers/projects/todos_controller_spec.rb
+++ b/spec/controllers/projects/todos_controller_spec.rb
@@ -36,7 +36,7 @@ describe Projects::TodosController do
expect(response).to have_gitlab_http_status(200)
expect(json_response['count']).to eq 1
- expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
+ expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}})
end
end
@@ -104,7 +104,7 @@ describe Projects::TodosController do
expect(response).to have_gitlab_http_status(200)
expect(json_response['count']).to eq 1
- expect(json_response['delete_path']).to match(/\/dashboard\/todos\/\d{1}/)
+ expect(json_response['delete_path']).to match(%r{/dashboard/todos/\d{1}})
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index b1f601a19e5..376b229ffc9 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -180,6 +180,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+
response
end
end
@@ -196,6 +197,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'user', mounted_as: 'avatar', id: user.id, filename: 'image.png'
+
response
end
end
@@ -220,6 +222,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+
response
end
end
@@ -239,6 +242,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+
response
end
end
@@ -291,6 +295,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'project', mounted_as: 'avatar', id: project.id, filename: 'image.png'
+
response
end
end
@@ -322,6 +327,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+
response
end
end
@@ -341,6 +347,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+
response
end
end
@@ -384,6 +391,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'group', mounted_as: 'avatar', id: group.id, filename: 'image.png'
+
response
end
end
@@ -420,6 +428,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+
response
end
end
@@ -439,6 +448,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+
response
end
end
@@ -491,6 +501,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'note', mounted_as: 'attachment', id: note.id, filename: 'image.png'
+
response
end
end
@@ -522,6 +533,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'appearance', mounted_as: 'header_logo', id: appearance.id, filename: 'dk.png'
+
response
end
end
@@ -541,6 +553,7 @@ describe UploadsController do
it_behaves_like 'content not cached without revalidation' do
subject do
get :show, model: 'appearance', mounted_as: 'logo', id: appearance.id, filename: 'dk.png'
+
response
end
end
diff --git a/spec/controllers/user_callouts_controller_spec.rb b/spec/controllers/user_callouts_controller_spec.rb
new file mode 100644
index 00000000000..48e2ff75cac
--- /dev/null
+++ b/spec/controllers/user_callouts_controller_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe UserCalloutsController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe "POST #create" do
+ subject { post :create, feature_name: feature_name, format: :json }
+
+ context 'with valid feature name' 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
+ expect { subject }.to change { UserCallout.count }.by(1)
+ end
+
+ it 'should return success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ 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
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'with invalid feature name' do
+ let(:feature_name) { 'bogus_feature_name' }
+
+ it 'should return bad request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
diff --git a/spec/factories/commits.rb b/spec/factories/commits.rb
index 84a8bc56640..d5d819d862a 100644
--- a/spec/factories/commits.rb
+++ b/spec/factories/commits.rb
@@ -23,7 +23,7 @@ FactoryBot.define do
end
after(:build) do |commit, evaluator|
- allow(commit).to receive(:author).and_return(evaluator.author || build(:author))
+ allow(commit).to receive(:author).and_return(evaluator.author || build_stubbed(:author))
end
trait :without_author do
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb
index 9d7d5e56611..cac56695319 100644
--- a/spec/factories/deployments.rb
+++ b/spec/factories/deployments.rb
@@ -3,13 +3,14 @@ FactoryBot.define do
sha '97de212e80737a608d939f648d959671fb0a0142'
ref 'master'
tag false
- user
+ user nil
project nil
deployable factory: :ci_build
environment factory: :environment
after(:build) do |deployment, evaluator|
deployment.project ||= deployment.environment.project
+ deployment.user ||= deployment.project.creator
unless deployment.project.repository_exists?
allow(deployment.project.repository).to receive(:create_ref)
diff --git a/spec/factories/events.rb b/spec/factories/events.rb
index ed275243ac9..5798b81ecad 100644
--- a/spec/factories/events.rb
+++ b/spec/factories/events.rb
@@ -1,7 +1,7 @@
FactoryBot.define do
factory :event do
project
- author factory: :user
+ author(factory: :user) { project.creator }
action Event::JOINED
trait(:created) { action Event::CREATED }
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 1512f5a0e58..8c531cf5909 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -18,7 +18,7 @@ FactoryBot.define do
end
trait :with_avatar do
- avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ avatar { fixture_file_upload('spec/fixtures/dk.png') }
end
trait :access_requestable do
diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb
index 71dc169c6a2..998080a3dd5 100644
--- a/spec/factories/issues.rb
+++ b/spec/factories/issues.rb
@@ -1,8 +1,8 @@
FactoryBot.define do
factory :issue do
title { generate(:title) }
- author
project
+ author { project.creator }
trait :confidential do
confidential true
diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb
index f0c43f3d6f5..23a98a899f1 100644
--- a/spec/factories/keys.rb
+++ b/spec/factories/keys.rb
@@ -5,6 +5,10 @@ FactoryBot.define do
title
key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate + ' dummy@gitlab.com' }
+ factory :key_without_comment do
+ key { Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate }
+ end
+
factory :deploy_key, class: 'DeployKey'
factory :personal_key do
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 40558c88d15..d26cb0c3417 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -1,9 +1,9 @@
FactoryBot.define do
factory :merge_request do
title { generate(:title) }
- author
association :source_project, :repository, factory: :project
target_project { source_project }
+ author { source_project.creator }
# $ git log --pretty=oneline feature..master
# 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 707ecbd6be5..3f4e408b3a6 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -6,7 +6,7 @@ FactoryBot.define do
factory :note do
project
note { generate(:title) }
- author
+ author { project&.creator || create(:user) }
on_issue
factory :note_on_commit, traits: [:on_commit]
@@ -122,11 +122,11 @@ FactoryBot.define do
end
trait :with_attachment do
- attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") }
+ attachment { fixture_file_upload(Rails.root.join( "spec/fixtures/dk.png"), "image/png") }
end
trait :with_svg_attachment do
- attachment { fixture_file_upload(Rails.root + "spec/fixtures/unsanitized.svg", "image/svg+xml") }
+ attachment { fixture_file_upload(Rails.root.join("spec/fixtures/unsanitized.svg"), "image/svg+xml") }
end
transient do
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
index 89d8248f9f4..db2eb4fc863 100644
--- a/spec/factories/project_wikis.rb
+++ b/spec/factories/project_wikis.rb
@@ -3,7 +3,7 @@ FactoryBot.define do
skip_create
project
- user factory: :user
+ user { project.creator }
initialize_with { new(project, user) }
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index d0f3911f730..20976977f21 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -90,7 +90,13 @@ FactoryBot.define do
end
trait :with_avatar do
- avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ avatar { fixture_file_upload('spec/fixtures/dk.png') }
+ end
+
+ trait :with_export do
+ after(:create) do |project, evaluator|
+ ProjectExportWorker.new.perform(project.creator.id, project.id)
+ end
end
trait :broken_storage do
diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb
index 80872067233..b0174dd06b7 100644
--- a/spec/factories/sent_notifications.rb
+++ b/spec/factories/sent_notifications.rb
@@ -1,7 +1,7 @@
FactoryBot.define do
factory :sent_notification do
project
- recipient factory: :user
+ recipient { project.creator }
noteable { create(:issue, project: project) }
reply_key { SentNotification.reply_key }
end
diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb
index 2ab9a56d255..dc12b562108 100644
--- a/spec/factories/snippets.rb
+++ b/spec/factories/snippets.rb
@@ -21,6 +21,7 @@ FactoryBot.define do
factory :project_snippet, parent: :snippet, class: :ProjectSnippet do
project
+ author { project.creator }
end
factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do
diff --git a/spec/factories/subscriptions.rb b/spec/factories/subscriptions.rb
index a4bc4e87b0a..8f7ab74ec70 100644
--- a/spec/factories/subscriptions.rb
+++ b/spec/factories/subscriptions.rb
@@ -1,7 +1,7 @@
FactoryBot.define do
factory :subscription do
- user
project
+ user { project.creator }
subscribable factory: :issue
end
end
diff --git a/spec/factories/timelogs.rb b/spec/factories/timelogs.rb
index af34b0681e2..b45f06b9a0a 100644
--- a/spec/factories/timelogs.rb
+++ b/spec/factories/timelogs.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :timelog do
time_spent 3600
- user
issue
+ user { issue.project.creator }
end
end
diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb
index 6a6de665dd1..94f8caedfa6 100644
--- a/spec/factories/todos.rb
+++ b/spec/factories/todos.rb
@@ -1,8 +1,8 @@
FactoryBot.define do
factory :todo do
project
- author
- user
+ author { project.creator }
+ user { project.creator }
target factory: :issue
action { Todo::ASSIGNED }
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index c39500faea1..ff3a2a76acc 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -1,24 +1,46 @@
FactoryBot.define do
factory :upload do
model { build(:project) }
- path { "uploads/-/system/project/avatar/avatar.jpg" }
size 100.kilobytes
uploader "AvatarUploader"
+ mount_point :avatar
+ secret nil
- trait :personal_snippet do
- model { build(:personal_snippet) }
+ # we should build a mount agnostic upload by default
+ transient do
+ filename 'myfile.jpg'
+ 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') }
+
+ trait :personal_snippet_upload do
uploader "PersonalFileUploader"
+ path { File.join(secret, filename) }
+ model { build(:personal_snippet) }
+ secret SecureRandom.hex
end
trait :issuable_upload do
- path { "#{SecureRandom.hex}/myfile.jpg" }
uploader "FileUploader"
+ path { File.join(secret, filename) }
+ secret SecureRandom.hex
end
trait :namespace_upload do
- path { "#{SecureRandom.hex}/myfile.jpg" }
model { build(:group) }
+ path { File.join(secret, filename) }
uploader "NamespaceFileUploader"
+ secret SecureRandom.hex
+ end
+
+ trait :attachment_upload do
+ transient do
+ mount_point :attachment
+ end
+
+ model { build(:note) }
+ uploader "AttachmentUploader"
end
end
end
diff --git a/spec/factories/user_callouts.rb b/spec/factories/user_callouts.rb
new file mode 100644
index 00000000000..528e442c14b
--- /dev/null
+++ b/spec/factories/user_callouts.rb
@@ -0,0 +1,7 @@
+FactoryBot.define do
+ factory :user_callout do
+ feature_name :gke_cluster_integration
+
+ user
+ end
+end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index e62e0b263ca..769fd656e7a 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -38,7 +38,7 @@ FactoryBot.define do
end
trait :with_avatar do
- avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ avatar { fixture_file_upload('spec/fixtures/dk.png') }
end
trait :two_factor_via_otp do
diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb
index ac3392b49f9..3693e5882f9 100644
--- a/spec/features/admin/admin_health_check_spec.rb
+++ b/spec/features/admin/admin_health_check_spec.rb
@@ -17,7 +17,7 @@ feature "Admin Health Check", :feature do
page.has_text? 'Health Check'
page.has_text? 'Health information can be retrieved'
- token = current_application_settings.health_check_access_token
+ token = Gitlab::CurrentSettings.health_check_access_token
expect(page).to have_content("Access token is #{token}")
expect(page).to have_selector('#health-check-token', text: token)
@@ -25,7 +25,7 @@ feature "Admin Health Check", :feature do
describe 'reload access token' do
it 'changes the access token' do
- orig_token = current_application_settings.health_check_access_token
+ orig_token = Gitlab::CurrentSettings.health_check_access_token
click_button 'Reset health check access token'
expect(page).to have_content('New health check access token has been generated!')
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index c1c54177167..a01c129defd 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -156,7 +156,7 @@ describe "Admin Runners" do
end
describe 'runners registration token' do
- let!(:token) { current_application_settings.runners_registration_token }
+ let!(:token) { Gitlab::CurrentSettings.runners_registration_token }
before do
visit admin_runners_path
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index 1218ea52227..cc0849d1cc6 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -38,12 +38,12 @@ feature 'Admin updates settings' do
uncheck 'Project export enabled'
click_button 'Save'
- expect(current_application_settings.gravatar_enabled).to be_falsey
- expect(current_application_settings.home_page_url).to eq "https://about.gitlab.com/"
- expect(current_application_settings.help_page_text).to eq "Example text"
- expect(current_application_settings.help_page_hide_commercial_content).to be_truthy
- expect(current_application_settings.help_page_support_url).to eq "http://example.com/help"
- expect(current_application_settings.project_export_enabled).to be_falsey
+ expect(Gitlab::CurrentSettings.gravatar_enabled).to be_falsey
+ expect(Gitlab::CurrentSettings.home_page_url).to eq "https://about.gitlab.com/"
+ expect(Gitlab::CurrentSettings.help_page_text).to eq "Example text"
+ expect(Gitlab::CurrentSettings.help_page_hide_commercial_content).to be_truthy
+ expect(Gitlab::CurrentSettings.help_page_support_url).to eq "http://example.com/help"
+ expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey
expect(page).to have_content "Application settings saved successfully"
end
diff --git a/spec/features/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index 782f42aab04..2d074c115dd 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -64,7 +64,7 @@ describe "User Feed" do
end
it 'has XHTML summaries in issue descriptions' do
- expect(body).to match /<hr ?\/>/
+ expect(body).to match %r{<hr ?/>}
end
it 'has XHTML summaries in notes' do
@@ -72,7 +72,7 @@ describe "User Feed" do
end
it 'has XHTML summaries in merge request descriptions' do
- expect(body).to match /Here is the fix: <a[^>]*><img[^>]*\/><\/a>/
+ expect(body).to match %r{Here is the fix: <a[^>]*><img[^>]*/></a>}
end
it 'has push event commit ID' do
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index a28b8905b65..62a2ec55b00 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -194,7 +194,7 @@ describe 'Commits' do
end
it 'includes the committed_date for each commit' do
- commits = project.repository.commits(branch_name)
+ commits = project.repository.commits(branch_name, limit: 40)
commits.each do |commit|
expect(page).to have_content("authored #{commit.authored_date.strftime("%b %d, %Y")}")
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 991d360ccaf..744041ac425 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -44,36 +44,38 @@ feature 'Dashboard Merge Requests' do
context 'merge requests exist' do
let!(:assigned_merge_request) do
- create(:merge_request, assignee: current_user, target_project: project, source_project: project)
+ create(:merge_request,
+ assignee: 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,
- target_project: public_project, source_project: forked_project
- )
+ target_project: public_project, source_project: forked_project,
+ author: create(:user))
end
let!(:authored_merge_request) do
create(:merge_request,
- source_branch: 'markdown', author: current_user,
- target_project: project, source_project: project
- )
+ source_branch: 'markdown',
+ source_project: project,
+ author: current_user)
end
let!(:authored_merge_request_from_fork) do
create(:merge_request,
source_branch: 'feature_conflict',
author: current_user,
- target_project: public_project, source_project: forked_project
- )
+ target_project: public_project, source_project: forked_project)
end
let!(:other_merge_request) do
create(:merge_request,
source_branch: 'fix',
- target_project: project, source_project: project
- )
+ source_project: project,
+ author: create(:user))
end
before do
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 1dd7547a7fc..31862b2e8f4 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -112,13 +112,6 @@ feature 'Expand and collapse diffs', :js do
wait_for_requests
end
- it 'makes a request to get the content' do
- ajax_uris = evaluate_script('ajaxUris')
-
- expect(ajax_uris).not_to be_empty
- expect(ajax_uris.first).to include('large_diff.md')
- end
-
it 'shows the diff content' do
expect(large_diff).to have_selector('.code')
expect(large_diff).not_to have_selector('.nothing-here-block')
diff --git a/spec/features/global_search_spec.rb b/spec/features/global_search_spec.rb
index 4f575613848..f8c4db1403c 100644
--- a/spec/features/global_search_spec.rb
+++ b/spec/features/global_search_spec.rb
@@ -22,7 +22,7 @@ feature 'Global search' do
click_button "Go"
select_filter("Issues")
- expect(page).to have_selector('.gl-pagination .page', count: 2)
+ expect(page).to have_selector('.gl-pagination .next')
end
end
end
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index cdf7aceb13c..450bc0ff8cf 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -3,40 +3,61 @@ require 'spec_helper'
feature 'Group issues page' do
include FilteredSearchHelpers
- let(:path) { issues_group_path(group) }
- let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
+ context 'with shared examples' do
+ let(:path) { issues_group_path(group) }
+ let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
- include_examples 'project features apply to issuables', Issue
+ include_examples 'project features apply to issuables', Issue
- context 'rss feed' do
- let(:access_level) { ProjectFeature::ENABLED }
+ context 'rss feed' do
+ let(:access_level) { ProjectFeature::ENABLED }
- context 'when signed in' do
- let(:user) { user_in_group }
+ context 'when signed in' do
+ let(:user) { user_in_group }
- it_behaves_like "it has an RSS button with current_user's RSS token"
- it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
- end
+ it_behaves_like "it has an RSS button with current_user's RSS token"
+ it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+ end
- context 'when signed out' do
- let(:user) { nil }
+ context 'when signed out' do
+ let(:user) { nil }
- it_behaves_like "it has an RSS button without an RSS token"
- it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ it_behaves_like "it has an RSS button without an RSS token"
+ it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+ end
end
- end
- context 'assignee', :js do
- let(:access_level) { ProjectFeature::ENABLED }
- let(:user) { user_in_group }
- let(:user2) { user_outside_group }
- let(:path) { issues_group_path(group) }
+ context 'assignee', :js do
+ let(:access_level) { ProjectFeature::ENABLED }
+ let(:user) { user_in_group }
+ let(:user2) { user_outside_group }
+ let(:path) { issues_group_path(group) }
+
+ it 'filters by only group users' do
+ filtered_search.set('assignee:')
- it 'filters by only group users' do
- filtered_search.set('assignee:')
+ expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
+ expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
+ end
+ end
+ end
- expect(find('#js-dropdown-assignee .filter-dropdown')).to have_content(user.name)
- expect(find('#js-dropdown-assignee .filter-dropdown')).not_to have_content(user2.name)
+ context 'issues list', :nested_groups do
+ let(:group) { create(:group)}
+ let(:subgroup) { create(:group, parent: group) }
+ let(:project) { create(:project, :public, group: group)}
+ let(:subgroup_project) { create(:project, :public, group: subgroup)}
+ let!(:issue) { create(:issue, project: project, title: 'root group issue') }
+ let!(:subgroup_issue) { create(:issue, project: subgroup_project, title: 'subgroup issue') }
+
+ it 'returns all group and subgroup issues' do
+ visit issues_group_path(group)
+
+ page.within('.issuable-list') do
+ expect(page).to have_selector('li.issue', count: 2)
+ expect(page).to have_content('root group issue')
+ expect(page).to have_content('subgroup issue')
+ end
end
end
end
diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb
index 53706ef84bc..a75ca1d42b3 100644
--- a/spec/features/issues/spam_issues_spec.rb
+++ b/spec/features/issues/spam_issues_spec.rb
@@ -9,7 +9,7 @@ describe 'New issue', :js do
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- current_application_settings.update!(
+ Gitlab::CurrentSettings.update!(
akismet_enabled: true,
akismet_api_key: 'testkey',
recaptcha_enabled: true,
@@ -34,6 +34,9 @@ describe 'New issue', :js do
click_button 'Submit issue'
+ # reCAPTCHA alerts when it can't contact the server, so just accept it and move on
+ page.driver.browser.switch_to.alert.accept
+
# it is impossible to test recaptcha automatically and there is no possibility to fill in recaptcha
# recaptcha verification is skipped in test environment and it always returns true
expect(page).not_to have_content('issue title')
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index a2b78a5e021..f13d78d24e3 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -259,6 +259,10 @@ describe 'GitLab Markdown' do
it 'includes VideoLinkFilter' do
expect(doc).to parse_video_links
end
+
+ it 'includes ColorFilter' do
+ expect(doc).to parse_colors
+ end
end
context 'wiki pipeline' do
@@ -320,6 +324,10 @@ describe 'GitLab Markdown' do
it 'includes VideoLinkFilter' do
expect(doc).to parse_video_links
end
+
+ it 'includes ColorFilter' do
+ expect(doc).to parse_colors
+ end
end
# Fake a `current_user` helper
diff --git a/spec/features/merge_request/user_awards_emoji_spec.rb b/spec/features/merge_request/user_awards_emoji_spec.rb
index 15a0878fb16..2f24cfbd9e3 100644
--- a/spec/features/merge_request/user_awards_emoji_spec.rb
+++ b/spec/features/merge_request/user_awards_emoji_spec.rb
@@ -3,7 +3,7 @@ require 'rails_helper'
describe 'Merge request > User awards emoji', :js do
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
- let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project, author: create(:user)) }
describe 'logged in' do
before do
diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb
index 61861d33952..19995559fae 100644
--- a/spec/features/merge_request/user_resolves_conflicts_spec.rb
+++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb
@@ -100,12 +100,12 @@ describe 'Merge request > User resolves conflicts', :js do
end
it 'shows a link to the conflict resolution page' do
- expect(page).to have_link('conflicts', href: /\/conflicts\Z/)
+ expect(page).to have_link('conflicts', href: %r{/conflicts\Z})
end
context 'in Inline view mode' do
before do
- click_link('conflicts', href: /\/conflicts\Z/)
+ click_link('conflicts', href: %r{/conflicts\Z})
end
include_examples "conflicts are resolved in Interactive mode"
@@ -114,7 +114,7 @@ describe 'Merge request > User resolves conflicts', :js do
context 'in Parallel view mode' do
before do
- click_link('conflicts', href: /\/conflicts\Z/)
+ click_link('conflicts', href: %r{/conflicts\Z})
click_button 'Side-by-side'
end
@@ -128,7 +128,7 @@ describe 'Merge request > User resolves conflicts', :js do
before do
visit project_merge_request_path(project, merge_request)
- click_link('conflicts', href: /\/conflicts\Z/)
+ click_link('conflicts', href: %r{/conflicts\Z})
end
it 'conflicts can not be resolved in Interactive mode' do
@@ -181,7 +181,7 @@ describe 'Merge request > User resolves conflicts', :js do
end
it 'does not show a link to the conflict resolution page' do
- expect(page).not_to have_link('conflicts', href: /\/conflicts\Z/)
+ expect(page).not_to have_link('conflicts', href: %r{/conflicts\Z})
end
it 'shows an error if the conflicts page is visited directly' do
diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
index 590210d44ef..3e83a549682 100644
--- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
+++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb
@@ -108,7 +108,7 @@ describe 'Merge request > User resolves diff notes and discussions', :js do
it 'shows resolved discussion when toggled' do
find(".timeline-content .discussion[data-discussion-id='#{note.discussion_id}'] .discussion-toggle-button").click
- expect(page.find(".timeline-content #note_#{note.noteable_id}")).to be_visible
+ expect(page.find(".timeline-content #note_#{note.id}")).to be_visible
end
end
diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
index fb73ab05f87..dbca279569a 100644
--- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
+++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb
@@ -61,7 +61,7 @@ describe 'Merge request > User selects branches for new MR', :js do
fill_in "merge_request_title", with: "Orphaned MR test"
click_button "Submit merge request"
- click_link "Check out branch"
+ click_button "Check out branch"
expect(page).to have_content 'git checkout -b orphaned-branch origin/orphaned-branch'
end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 821ce88a402..f51001edcd7 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -18,7 +18,7 @@ feature 'test coverage badge' do
show_test_coverage_badge
- expect_coverage_badge('95%')
+ expect_coverage_badge('95.00%')
end
scenario 'user requests coverage badge for specific job' do
@@ -30,7 +30,7 @@ feature 'test coverage badge' do
show_test_coverage_badge(job: 'coverage')
- expect_coverage_badge('85%')
+ expect_coverage_badge('85.00%')
end
scenario 'user requests coverage badge for pipeline without coverage' do
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 69e4c9f04a1..89d3bd24b89 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -17,12 +17,15 @@ feature 'Editing file blob', :js do
sign_in(user)
end
- def edit_and_commit
+ def edit_and_commit(commit_changes: true)
wait_for_requests
find('.js-edit-blob').click
find('#editor')
execute_script('ace.edit("editor").setValue("class NextFeature\nend\n")')
- click_button 'Commit changes'
+
+ if commit_changes
+ click_button 'Commit changes'
+ end
end
context 'from MR diff' do
@@ -39,13 +42,26 @@ feature 'Editing file blob', :js do
context 'from blob file path' do
before do
visit project_blob_path(project, tree_join(branch, file_path))
- edit_and_commit
end
it 'updates content' do
+ edit_and_commit
+
expect(page).to have_content 'successfully committed'
expect(page).to have_content 'NextFeature'
end
+
+ it 'previews content' do
+ edit_and_commit(commit_changes: false)
+ click_link 'Preview changes'
+ wait_for_requests
+
+ old_line_count = page.all('.line_holder.old').size
+ new_line_count = page.all('.line_holder.new').size
+
+ expect(old_line_count).to be > 0
+ expect(new_line_count).to be > 0
+ end
end
end
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index 8953b30bebf..94bde723e2f 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -95,7 +95,7 @@ feature 'Gcp Cluster', :js do
context 'when user disables the cluster' do
before do
- page.find(:css, '.js-toggle-cluster').click
+ page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
page.within('#cluster-integration') { click_button 'Save changes' }
end
diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb
index a519b9f9c7e..b9ab434c259 100644
--- a/spec/features/projects/clusters/user_spec.rb
+++ b/spec/features/projects/clusters/user_spec.rb
@@ -62,7 +62,7 @@ feature 'User Cluster', :js do
context 'when user disables the cluster' do
before do
- page.find(:css, '.js-toggle-cluster').click
+ page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
fill_in 'cluster_name', with: 'dev-cluster'
page.within('#cluster-integration') { click_button 'Save changes' }
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index eae2910a8f6..497a50bebe4 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -37,13 +37,13 @@ feature 'Clusters', :js do
context 'inline update of cluster' do
it 'user can update cluster' do
- expect(page).to have_selector('.js-toggle-cluster-list')
+ expect(page).to have_selector('.js-project-feature-toggle')
end
context 'with sucessfull request' do
it 'user sees updated cluster' do
expect do
- page.find('.js-toggle-cluster-list').click
+ page.find('.js-project-feature-toggle').click
wait_for_requests
end.to change { cluster.reload.enabled }
@@ -57,7 +57,7 @@ feature 'Clusters', :js do
expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
- page.find('.js-toggle-cluster-list').click
+ page.find('.js-project-feature-toggle').click
expect(page).to have_content('Something went wrong on our end.')
expect(page).to have_selector('.is-checked')
diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb
index e76bc6f1220..c1fccf4a40b 100644
--- a/spec/features/projects/import_export/namespace_export_file_spec.rb
+++ b/spec/features/projects/import_export/namespace_export_file_spec.rb
@@ -1,44 +1,37 @@
require 'spec_helper'
feature 'Import/Export - Namespace export file cleanup', :js do
- let(:export_path) { "#{Dir.tmpdir}/import_file_spec" }
- let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys }
+ let(:export_path) { Dir.mktmpdir('namespace_export_file_spec') }
- let(:project) { create(:project) }
-
- background do
- allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+ before do
+ allow(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
end
after do
FileUtils.rm_rf(export_path, secure: true)
end
- context 'admin user' do
+ shared_examples_for 'handling project exports on namespace change' do
+ let!(:old_export_path) { project.export_path }
+
before do
sign_in(create(:admin))
+
+ setup_export_project
end
context 'moving the namespace' do
- scenario 'removes the export file' do
- setup_export_project
-
- old_export_path = project.export_path.dup
-
+ it 'removes the export file' do
expect(File).to exist(old_export_path)
- project.namespace.update(path: 'new_path')
+ project.namespace.update!(path: build(:namespace).path)
expect(File).not_to exist(old_export_path)
end
end
context 'deleting the namespace' do
- scenario 'removes the export file' do
- setup_export_project
-
- old_export_path = project.export_path.dup
-
+ it 'removes the export file' do
expect(File).to exist(old_export_path)
project.namespace.destroy
@@ -46,17 +39,29 @@ feature 'Import/Export - Namespace export file cleanup', :js do
expect(File).not_to exist(old_export_path)
end
end
+ end
- def setup_export_project
- visit edit_project_path(project)
+ describe 'legacy storage' do
+ let(:project) { create(:project) }
- expect(page).to have_content('Export project')
+ it_behaves_like 'handling project exports on namespace change'
+ end
+
+ describe 'hashed storage' do
+ let(:project) { create(:project, :hashed) }
- find(:link, 'Export project').send_keys(:return)
+ it_behaves_like 'handling project exports on namespace change'
+ end
- visit edit_project_path(project)
+ def setup_export_project
+ visit edit_project_path(project)
- expect(page).to have_content('Download export')
- end
+ expect(page).to have_content('Export project')
+
+ find(:link, 'Export project').send_keys(:return)
+
+ visit edit_project_path(project)
+
+ expect(page).to have_content('Download export')
end
end
diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb
index 3198798306c..4cf48098401 100644
--- a/spec/features/projects/members/share_with_group_spec.rb
+++ b/spec/features/projects/members/share_with_group_spec.rb
@@ -122,7 +122,7 @@ feature 'Project > Members > Share with Group', :js do
select2 group.id, from: '#link_group_id'
fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
- page.find('body').click
+ click_on 'share-with-group-tab'
find('.btn-create').click
end
diff --git a/spec/features/projects/pipeline_schedules_spec.rb b/spec/features/projects/pipeline_schedules_spec.rb
index fa2f7a1fd78..65e24862d43 100644
--- a/spec/features/projects/pipeline_schedules_spec.rb
+++ b/spec/features/projects/pipeline_schedules_spec.rb
@@ -168,11 +168,11 @@ feature 'Pipeline Schedules', :js do
scenario 'user sees the new variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.pipeline-variable-list') do
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('AAA')
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('AAA123')
- expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-key-input").value).to eq('BBB')
- expect(find(".pipeline-variable-row:nth-child(2) .pipeline-variable-value-input").value).to eq('BBB123')
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('AAA')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('AAA123')
+ expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-key").value).to eq('BBB')
+ expect(find(".ci-variable-row:nth-child(2) .js-ci-variable-input-value", visible: false).value).to eq('BBB123')
end
end
end
@@ -185,16 +185,18 @@ feature 'Pipeline Schedules', :js do
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- all('[name="schedule[variables_attributes][][key]"]')[0].set('foo')
- all('[name="schedule[variables_attributes][][value]"]')[0].set('bar')
+
+ find('.js-ci-variable-list-section .js-secret-value-reveal-button').click
+ first('.js-ci-variable-input-key').set('foo')
+ first('.js-ci-variable-input-value').set('bar')
click_button 'Save pipeline schedule'
end
scenario 'user sees the updated variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.pipeline-variable-list') do
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('foo')
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('bar')
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('foo')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('bar')
end
end
end
@@ -207,15 +209,15 @@ feature 'Pipeline Schedules', :js do
visit_pipelines_schedules
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- find('.pipeline-variable-list .pipeline-variable-row-remove-button').click
+ find('.ci-variable-list .ci-variable-row-remove-button').click
click_button 'Save pipeline schedule'
end
scenario 'user does not see the removed variable in edit window' do
find(".content-list .pipeline-schedule-table-row:nth-child(1) .btn-group a[title='Edit']").click
- page.within('.pipeline-variable-list') do
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-key-input").value).to eq('')
- expect(find(".pipeline-variable-row:nth-child(1) .pipeline-variable-value-input").value).to eq('')
+ page.within('.ci-variable-list') do
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-key").value).to eq('')
+ expect(find(".ci-variable-row:nth-child(1) .js-ci-variable-input-value", visible: false).value).to eq('')
end
end
end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index 949d90a50ff..4d2a08afecc 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
-describe 'User updates wiki page' do
+# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
+describe 'User updates wiki page', :skip_gitaly_mock do
let(:user) { create(:user) }
before do
@@ -143,6 +144,7 @@ describe 'User updates wiki page' do
expect(page).to have_field('wiki[message]', with: 'Update home')
fill_in(:wiki_content, with: 'My awesome wiki!')
+
click_button('Save changes')
expect(page).to have_content('Home')
@@ -151,4 +153,74 @@ describe 'User updates wiki page' do
end
end
end
+
+ context 'when the page is in a subdir' do
+ let!(:project) { create(:project, namespace: user.namespace) }
+ let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) }
+ let(:page_name) { 'page_name' }
+ let(:page_dir) { "foo/bar/#{page_name}" }
+ let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, attrs: { title: page_dir, content: 'Home page' }) }
+
+ before do
+ visit(project_wiki_edit_path(project, wiki_page))
+ end
+
+ it 'moves the page to the root folder' do
+ fill_in(:wiki_title, with: "/#{page_name}")
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, page_name))
+ end
+
+ it 'moves the page to other dir' do
+ new_page_dir = "foo1/bar1/#{page_name}"
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, new_page_dir))
+ end
+
+ it 'remains in the same place if title has not changed' do
+ original_path = project_wiki_path(project, wiki_page)
+
+ fill_in(:wiki_title, with: page_name)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(original_path)
+ end
+
+ it 'can be moved to a different dir with a different name' do
+ new_page_dir = "foo1/bar1/new_page_name"
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, new_page_dir))
+ end
+
+ it 'can be renamed and moved to the root folder' do
+ new_name = 'new_page_name'
+
+ fill_in(:wiki_title, with: "/#{new_name}")
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, new_name))
+ end
+
+ it 'squishes the title before creating the page' do
+ new_page_dir = " foo1 / bar1 / #{page_name} "
+
+ fill_in(:wiki_title, with: new_page_dir)
+
+ click_button('Save changes')
+
+ expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}"))
+ end
+ end
end
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index ff325aeadd3..e37436838fd 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
-describe 'User views a wiki page' do
+# Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
+describe 'User views a wiki page', :skip_gitaly_mock do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let(:wiki_page) do
diff --git a/spec/features/user_page_spec.rb b/spec/features/user_page_spec.rb
new file mode 100644
index 00000000000..19c587e53c8
--- /dev/null
+++ b/spec/features/user_page_spec.rb
@@ -0,0 +1,107 @@
+require 'spec_helper'
+
+describe 'User page', :js do
+ let!(:user) { create :user }
+ let!(:private_project) do
+ create :project, :private, name: 'private', namespace: user.namespace do |project|
+ project.add_master(user)
+ end
+ end
+
+ let!(:internal_project) do
+ create :project, :internal, name: 'internal', namespace: user.namespace do |project|
+ project.add_master(user)
+ end
+ end
+
+ let!(:public_project) do
+ create :project, :public, name: 'public', namespace: user.namespace do |project|
+ project.add_master(user)
+ end
+ end
+
+ def click_nav_link(name)
+ page.within '.nav-links' do
+ click_link name
+ end
+ end
+
+ context 'when not signed in' do
+ it 'renders user public project' do
+ visit user_path(user)
+ click_nav_link('Personal projects')
+
+ expect(page).to have_css('.tab-content #projects.active')
+ expect(title).to start_with(user.name)
+
+ expect(page).to have_content(public_project.name)
+ expect(page).not_to have_content(private_project.name)
+ expect(page).not_to have_content(internal_project.name)
+ end
+ end
+
+ context 'when signed in as another user' do
+ let(:another_user) { create :user }
+
+ before do
+ sign_in(another_user)
+ end
+
+ it 'renders user public and internal projects' do
+ visit user_path(user)
+ click_nav_link('Personal projects')
+
+ expect(title).to start_with(user.name)
+
+ expect(page).not_to have_content(private_project.name)
+ expect(page).to have_content(public_project.name)
+ expect(page).to have_content(internal_project.name)
+ end
+ end
+
+ context 'when signed in as user' do
+ before do
+ sign_in(user)
+ end
+
+ describe 'personal projects' do
+ it 'renders all user projects' do
+ visit user_path(user)
+ click_nav_link('Personal projects')
+
+ expect(title).to start_with(user.name)
+
+ expect(page).to have_content(private_project.name)
+ expect(page).to have_content(public_project.name)
+ expect(page).to have_content(internal_project.name)
+ end
+ end
+
+ describe 'contributed projects' do
+ context 'when user has contributions' do
+ let(:contributed_project) do
+ create :project, :public, :empty_repo
+ end
+
+ before do
+ Issues::CreateService.new(contributed_project, user, { title: 'Bug in old browser' }).execute
+ event = create(:push_event, project: contributed_project, author: user)
+ create(:push_event_payload, event: event, commit_count: 3)
+ end
+
+ it 'renders contributed project' do
+ visit user_path(user)
+
+ expect(title).to start_with(user.name)
+ expect(page).to have_css('.js-contrib-calendar')
+
+ click_nav_link('Contributed projects')
+
+ page.within '#contributed' do
+ expect(page).to have_content(contributed_project.name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/group_projects_finder_spec.rb b/spec/finders/group_projects_finder_spec.rb
index 27a09d7c6f5..be80ee7d767 100644
--- a/spec/finders/group_projects_finder_spec.rb
+++ b/spec/finders/group_projects_finder_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe GroupProjectsFinder do
let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
let(:current_user) { create(:user) }
let(:options) { {} }
@@ -12,6 +13,8 @@ describe GroupProjectsFinder do
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::MASTER, group: group)
@@ -35,11 +38,31 @@ describe GroupProjectsFinder do
context "only owned" do
let(:options) { { only_owned: true } }
- it { is_expected.to match_array([private_project, public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to match_array([private_project, public_project]) }
+ end
end
context "all" do
- it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project, subgroup_project, subgroup_private_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, private_project, public_project]) }
+ end
end
end
@@ -71,9 +94,20 @@ describe GroupProjectsFinder do
context "without external user" do
before do
private_project.add_master(current_user)
+ subgroup_private_project.add_master(current_user)
end
- it { is_expected.to match_array([private_project, public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([private_project, public_project, subgroup_project, subgroup_private_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to match_array([private_project, public_project]) }
+ end
end
context "with external user" do
@@ -81,12 +115,32 @@ describe GroupProjectsFinder do
current_user.update_attributes(external: true)
end
- it { is_expected.to eq([public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([public_project, subgroup_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to eq([public_project]) }
+ end
end
end
context "all" do
- it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project, subgroup_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to match_array([shared_project_3, shared_project_2, shared_project_1, public_project]) }
+ end
end
end
@@ -100,7 +154,17 @@ describe GroupProjectsFinder do
context "only owned" do
let(:options) { { only_owned: true } }
- it { is_expected.to eq([public_project]) }
+ context 'with subgroups projects', :nested_groups do
+ before do
+ options[:include_subgroups] = true
+ end
+
+ it { is_expected.to match_array([public_project, subgroup_project]) }
+ end
+
+ context 'without subgroups projects' do
+ it { is_expected.to eq([public_project]) }
+ end
end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 47fd98234f9..abb7631d7d7 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -3,13 +3,17 @@ require 'spec_helper'
describe IssuesFinder do
set(:user) { create(:user) }
set(:user2) { create(:user) }
- set(:project1) { create(:project) }
+ 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) }
set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') }
set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.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) }
@@ -25,10 +29,12 @@ describe IssuesFinder do
project1.add_master(user)
project2.add_developer(user)
project2.add_developer(user2)
+ project3.add_developer(user)
issue1
issue2
issue3
+ issue4
award_emoji1
award_emoji2
@@ -39,7 +45,7 @@ describe IssuesFinder do
let(:scope) { 'all' }
it 'returns all issues' do
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
context 'filtering by assignee ID' do
@@ -50,6 +56,26 @@ describe IssuesFinder do
end
end
+ context 'filtering by group_id' do
+ let(:params) { { group_id: group.id } }
+
+ context 'when include_subgroup param not set' do
+ it 'returns all group issues' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+
+ context 'when include_subgroup param is true', :nested_groups do
+ before do
+ params[:include_subgroups] = true
+ end
+
+ it 'returns all group and subgroup issues' do
+ expect(issues).to contain_exactly(issue1, issue4)
+ end
+ end
+ end
+
context 'filtering by author ID' do
let(:params) { { author_id: user2.id } }
@@ -87,7 +113,7 @@ describe IssuesFinder do
let(:params) { { milestone_title: Milestone::None.title } }
it 'returns issues with no milestone' do
- expect(issues).to contain_exactly(issue2, issue3)
+ expect(issues).to contain_exactly(issue2, issue3, issue4)
end
end
@@ -185,7 +211,7 @@ describe IssuesFinder do
let(:params) { { label_name: Label::None.title } }
it 'returns issues with no labels' do
- expect(issues).to contain_exactly(issue1, issue3)
+ expect(issues).to contain_exactly(issue1, issue3, issue4)
end
end
@@ -210,7 +236,7 @@ describe IssuesFinder do
let(:params) { { state: 'opened' } }
it 'returns only opened issues' do
- expect(issues).to contain_exactly(issue1, issue2, issue3)
+ expect(issues).to contain_exactly(issue1, issue2, issue3, issue4)
end
end
@@ -226,7 +252,7 @@ describe IssuesFinder do
let(:params) { { state: 'all' } }
it 'returns all issues' do
- expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue)
+ expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue, issue4)
end
end
@@ -234,7 +260,7 @@ describe IssuesFinder do
let(:params) { { state: 'invalid_state' } }
it 'returns all issues' do
- expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue)
+ expect(issues).to contain_exactly(issue1, issue2, issue3, closed_issue, issue4)
end
end
end
@@ -338,7 +364,7 @@ describe IssuesFinder do
end
it "doesn't return issues if feature disabled" do
- [project1, project2].each do |project|
+ [project1, project2, project3].each do |project|
project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
end
@@ -351,7 +377,7 @@ describe IssuesFinder do
it 'returns the number of rows for the default state' do
finder = described_class.new(user)
- expect(finder.row_count).to eq(3)
+ expect(finder.row_count).to eq(4)
end
it 'returns the number of rows for a given state' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 687ffaec7cc..9385c892c9e 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -6,31 +6,36 @@ describe MergeRequestsFinder do
let(:user) { create :user }
let(:user2) { create :user }
- let(:project1) { create(:project, :public) }
+ let(:group) { create(:group) }
+ let(:subgroup) { create(:group, parent: group) }
+ let(:project1) { create(:project, :public, group: group) }
let(:project2) { fork_project(project1, user) }
let(:project3) do
p = fork_project(project1, user)
p.update!(archived: true)
p
end
+ let(:project4) { create(:project, :public, group: subgroup) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
let!(:merge_request2) { create(:merge_request, :simple, 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) }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
+ let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) }
before do
project1.add_master(user)
project2.add_developer(user)
project3.add_developer(user)
project2.add_developer(user2)
+ project4.add_developer(user)
end
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(3)
+ expect(merge_requests.size).to eq(4)
end
it 'filters by project' do
@@ -39,10 +44,26 @@ describe MergeRequestsFinder do
expect(merge_requests.size).to eq(1)
end
+ it 'filters by group' do
+ params = { group_id: group.id }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests.size).to eq(2)
+ 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.size).to eq(3)
+ 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.size).to eq(4)
end
it 'filters by iid' do
@@ -73,14 +94,14 @@ describe MergeRequestsFinder do
end
context 'with created_after and created_before params' do
- let(:project4) { create(:project, forked_from_project: project1) }
+ 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,
- source_project: project4,
+ source_project: new_project,
target_project: project1)
end
@@ -89,12 +110,12 @@ describe MergeRequestsFinder do
:simple,
author: user,
created_at: 1.week.ago,
- source_project: project4,
- target_project: project4)
+ source_project: new_project,
+ target_project: new_project)
end
before do
- project4.add_master(user)
+ new_project.add_master(user)
end
it 'filters by created_after' do
@@ -106,7 +127,7 @@ describe MergeRequestsFinder do
end
it 'filters by created_before' do
- params = { project_id: project4.id, created_before: old_merge_request.created_at + 1.second }
+ params = { project_id: new_project.id, created_before: old_merge_request.created_at + 1.second }
merge_requests = described_class.new(user, params).execute
@@ -119,7 +140,7 @@ describe MergeRequestsFinder do
it 'returns the number of rows for the default state' do
finder = described_class.new(user)
- expect(finder.row_count).to eq(3)
+ expect(finder.row_count).to eq(4)
end
it 'returns the number of rows for a given state' do
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/basic.json b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
index bf330d8278c..2d815be32a6 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/basic.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/basic.json
@@ -2,12 +2,16 @@
"type": ["object", "null"],
"required": [
"id",
+ "name",
+ "username",
"state",
"avatar_url",
"web_url"
],
"properties": {
"id": { "type": "integer" },
+ "name": { "type": "string" },
+ "username": { "type": "string" },
"state": { "type": "string" },
"avatar_url": { "type": "string" },
"web_url": { "type": "string" }
diff --git a/spec/fixtures/emails/attachment.eml b/spec/fixtures/emails/attachment.eml
index f25c3d1a449..b3a30b3221b 100644
--- a/spec/fixtures/emails/attachment.eml
+++ b/spec/fixtures/emails/attachment.eml
@@ -91,7 +91,7 @@ x #ccc solid;padding-left:1ex"><div>
adding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
+ <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
532f8ad0.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"Neil" style=3D"m=
ax-width:694px" width=3D"45" height=3D"45">
</td>
@@ -121,7 +121,7 @@ nk">@eviltrout</a> Any idea why it showed up in suggested topics? </p>
<div style=3D"color:#666">
<p>To respond, reply to this email or visit <a href=3D"http://meta.disc=
ourse.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"co=
-lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back=
+lor:#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back=
-up-in-suggested-topics/11005/5</a> in your browser.</p>
</div>
@@ -132,12 +132,12 @@ lor:#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back=
lpadding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
+ <img src=3D"https://www.gravatar.com/avatar/42776c4982dff1fa45ee8248=
532f8ad0.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"Neil" style=3D"m=
ax-width:694px" width=3D"45" height=3D"45">
</td>
<td>
- <a href=3D"http://meta.discourse.org/users/neil" style=3D"font-size=
+ <a href=3D"https://meta.discourse.org/users/neil" style=3D"font-size=
:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif;c=
olor:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">Neil<=
/a><br>
@@ -155,12 +155,12 @@ vember 19</span>
adding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
+ <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
72073819.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"riking" style=3D=
"max-width:694px" width=3D"45" height=3D"45">
</td>
<td>
- <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si=
+ <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si=
ze:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif=
;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik=
ing</a><br>
@@ -173,7 +173,7 @@ vember 19</span>
<td style=3D"padding-top:5px" colspan=3D"2">
<p style=3D"margin-top:0"><u></u></p><div>
<div></div>
-<img width=3D"20" height=3D"20" src=3D"http://www.gravatar.com/avatar/51d62=
+<img width=3D"20" height=3D"20" src=3D"https://www.gravatar.com/avatar/51d62=
3f33f8b83095db84ff35e15dbe8.png?s=3D40&amp;r=3Dpg&amp;d=3Didenticon" style=
=3D"max-width:694px">codinghorror:</div>
<blockquote><p style=3D"margin-top:0">I can&#39;t even find that topic by n=
@@ -193,12 +193,12 @@ uld be invisible to me, and not showing up in Suggested Topics.</p>
adding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/51d623f33f8b83095db84ff3=
+ <img src=3D"https://www.gravatar.com/avatar/51d623f33f8b83095db84ff3=
5e15dbe8.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"codinghorror" st=
yle=3D"max-width:694px" width=3D"45" height=3D"45">
</td>
<td>
- <a href=3D"http://meta.discourse.org/users/codinghorror" style=3D"f=
+ <a href=3D"https://meta.discourse.org/users/codinghorror" style=3D"f=
ont-size:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans=
-serif;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blan=
k">codinghorror</a><br>
@@ -219,12 +219,12 @@ rout" target=3D"_blank">@eviltrout</a>? I can&#39;t even find that topic by=
adding=3D"0" border=3D"0"><tbody>
<tr>
<td style=3D"vertical-align:top;width:55px">
- <img src=3D"http://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
+ <img src=3D"https://www.gravatar.com/avatar/5120fc4e345db0d1a9648882=
72073819.png?s=3D45&amp;r=3Dpg&amp;d=3Didenticon" title=3D"riking" style=3D=
"max-width:694px" width=3D"45" height=3D"45">
</td>
<td>
- <a href=3D"http://meta.discourse.org/users/riking" style=3D"font-si=
+ <a href=3D"https://meta.discourse.org/users/riking" style=3D"font-si=
ze:13px;font-family:&#39;lucida grande&#39;,tahoma,verdana,arial,sans-serif=
;color:#3b5998;text-decoration:none;font-weight:bold" target=3D"_blank">rik=
ing</a><br>
@@ -241,7 +241,7 @@ lar spam post, and it was promptly deleted/hidden, but it just popped up in=
<p style=3D"margin-top:0"></p>
<div><a href=3D"//cdn.discourse.org/uploads/meta_discourse/2158/50b8b49557c=
-b249e.png" target=3D"_blank"><img src=3D"http://cdn.discourse.org/uploads/m=
+b249e.png" target=3D"_blank"><img src=3D"https://cdn.discourse.org/uploads/m=
eta_discourse/_optimized/ab1/c92/acd2c33402_584x134.png" width=3D"584" heig=
ht=3D"134" style=3D"max-width:694px"><div>
@@ -257,12 +257,12 @@ ht=3D"134" style=3D"max-width:694px"><div>
<div style=3D"color:#666">
<p>To respond, reply to this email or visit <a href=3D"http://meta.discours=
e.org/t/spam-post-pops-back-up-in-suggested-topics/11005/5" style=3D"color:=
-#666" target=3D"_blank">http://meta.discourse.org/t/spam-post-pops-back-up-=
+#666" target=3D"_blank">https://meta.discourse.org/t/spam-post-pops-back-up-=
in-suggested-topics/11005/5</a> in your browser.</p>
</div>
<div style=3D"color:#666">
-<p>To unsubscribe from these emails, visit your <a href=3D"http://meta.disc=
+<p>To unsubscribe from these emails, visit your <a href=3D"https://meta.disc=
ourse.org/user_preferences" style=3D"color:#666" target=3D"_blank">user pre=
ferences</a>.</p>
</div>
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 71abb6da607..da32a46675f 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -280,6 +280,18 @@ However the wrapping tags cannot be mixed as such:
![My Video](/assets/videos/gitlab-demo.mp4)
+### Colors
+
+`#F00`
+`#F00A`
+`#FF0000`
+`#FF0000AA`
+`RGB(0,255,0)`
+`RGB(0%,100%,0%)`
+`RGBA(0,255,0,0.7)`
+`HSL(540,70%,50%)`
+`HSLA(540,70%,50%,0.7)`
+
### Mermaid
> If this is not rendered correctly, see
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 5c5d53877a6..f7a4a7afced 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -100,7 +100,7 @@ describe ApplicationHelper do
end
it 'returns a generic avatar' do
- expect(helper.gravatar_icon(user_email)).to match('no_avatar.png')
+ expect(helper.gravatar_icon(user_email)).to match_asset_path('no_avatar.png')
end
end
@@ -110,14 +110,14 @@ describe ApplicationHelper do
end
it 'returns a generic avatar when email is blank' do
- expect(helper.gravatar_icon('')).to match('no_avatar.png')
+ expect(helper.gravatar_icon('')).to match_asset_path('no_avatar.png')
end
it 'returns a valid Gravatar URL' do
stub_config_setting(https: false)
expect(helper.gravatar_icon(user_email))
- .to match('http://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
+ .to match('https://www.gravatar.com/avatar/b58c6f14d292556214bd64909bcdb118')
end
it 'uses HTTPs when configured' do
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 32432ee1e81..5f608fe18d9 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -105,7 +105,7 @@ describe GroupsHelper do
it 'outputs the groups in the correct order' do
expect(helper.group_title(very_deep_nested_group))
- .to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m)
+ .to match(%r{<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*</li>.*<a.*>#{very_deep_nested_group.name}</a>}m)
end
end
@@ -120,7 +120,7 @@ describe GroupsHelper do
let(:possible_help_texts) do
{
default_help: "This setting will be applied to all subgroups unless overridden by a group owner",
- ancestor_locked_but_you_can_override: /This setting is applied on <a .+>.+<\/a>\. You can override the setting or .+/,
+ ancestor_locked_but_you_can_override: %r{This setting is applied on <a .+>.+</a>\. You can override the setting or .+},
ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/,
ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/
}
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 0286d36952c..619baa78bfa 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -104,7 +104,7 @@ 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 %r{has-tooltip}
+ expect(link_to_label(label, tooltip: false)).not_to match /has-tooltip/
end
end
end
diff --git a/spec/helpers/user_callouts_helper_spec.rb b/spec/helpers/user_callouts_helper_spec.rb
new file mode 100644
index 00000000000..27455705d23
--- /dev/null
+++ b/spec/helpers/user_callouts_helper_spec.rb
@@ -0,0 +1,47 @@
+require "spec_helper"
+
+describe UserCalloutsHelper do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ describe '.show_gke_cluster_integration_callout?' do
+ let(:project) { create(:project) }
+
+ subject { helper.show_gke_cluster_integration_callout?(project) }
+
+ context 'when user can create a cluster' do
+ before do
+ allow(helper).to receive(:can?).with(anything, :create_cluster, anything)
+ .and_return(true)
+ end
+
+ context 'when user has not dismissed' do
+ before do
+ allow(helper).to receive(:user_dismissed?).and_return(false)
+ end
+
+ it { is_expected.to be true }
+ end
+
+ context 'when user dismissed' do
+ before do
+ allow(helper).to receive(:user_dismissed?).and_return(true)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+
+ context 'when user can not create a cluster' do
+ before do
+ allow(helper).to receive(:can?).with(anything, :create_cluster, anything)
+ .and_return(false)
+ end
+
+ it { is_expected.to be false }
+ end
+ end
+end
diff --git a/spec/helpers/version_check_helper_spec.rb b/spec/helpers/version_check_helper_spec.rb
index fa8cfda3b86..9d4e34abef5 100644
--- a/spec/helpers/version_check_helper_spec.rb
+++ b/spec/helpers/version_check_helper_spec.rb
@@ -4,7 +4,7 @@ describe VersionCheckHelper do
describe '#version_status_badge' do
it 'should return nil if not dev environment and not enabled' do
allow(Rails.env).to receive(:production?) { false }
- allow(helper.current_application_settings).to receive(:version_check_enabled) { false }
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { false }
expect(helper.version_status_badge).to be(nil)
end
@@ -12,7 +12,7 @@ describe VersionCheckHelper do
context 'when production and enabled' do
before do
allow(Rails.env).to receive(:production?) { true }
- allow(helper.current_application_settings).to receive(:version_check_enabled) { true }
+ allow(Gitlab::CurrentSettings.current_application_settings).to receive(:version_check_enabled) { true }
allow_any_instance_of(VersionCheck).to receive(:url) { 'https://version.host.com/check.svg?gitlab_info=xxx' }
@image_tag = helper.version_status_badge
@@ -27,7 +27,7 @@ describe VersionCheckHelper do
end
it 'should have a VersionCheck url as the src' do
- expect(@image_tag).to match(/src="https:\/\/version\.host\.com\/check\.svg\?gitlab_info=xxx"/)
+ expect(@image_tag).to match(%r{src="https://version\.host\.com/check\.svg\?gitlab_info=xxx"})
end
end
end
diff --git a/spec/initializers/grape_route_helpers_fix_spec.rb b/spec/initializers/grape_route_helpers_fix_spec.rb
new file mode 100644
index 00000000000..2cf5924128f
--- /dev/null
+++ b/spec/initializers/grape_route_helpers_fix_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+require_relative '../../config/initializers/grape_route_helpers_fix'
+
+describe 'route shadowing' do
+ include GrapeRouteHelpers::NamedRouteMatcher
+
+ it 'does not occur' do
+ path = api_v4_projects_merge_requests_path(id: 1)
+ expect(path).to eq('/api/v4/projects/1/merge_requests')
+
+ path = api_v4_projects_merge_requests_path(id: 1, merge_request_iid: 3)
+ expect(path).to eq('/api/v4/projects/1/merge_requests/3')
+ end
+end
diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb
index a11824d0ac5..838ca9fabef 100644
--- a/spec/initializers/settings_spec.rb
+++ b/spec/initializers/settings_spec.rb
@@ -24,7 +24,7 @@ describe Settings do
expect(described_class.host_without_www('http://foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('http://www.foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('http://secure.foo.com')).to eq 'secure.foo.com'
- expect(described_class.host_without_www('http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
+ expect(described_class.host_without_www('https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com'
expect(described_class.host_without_www('https://foo.com')).to eq 'foo.com'
expect(described_class.host_without_www('https://www.foo.com')).to eq 'foo.com'
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 2aa4fb1f6c6..cf3a76d0d2e 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -1,3 +1,5 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
describe('Api', () => {
@@ -7,20 +9,17 @@ describe('Api', () => {
api_version: dummyApiVersion,
relative_url_root: dummyUrlRoot,
};
- const dummyResponse = 'hello from outer space!';
- const sendDummyResponse = () => {
- const deferred = $.Deferred();
- deferred.resolve(dummyResponse);
- return deferred.promise();
- };
let originalGon;
+ let mock;
beforeEach(() => {
+ mock = new MockAdapter(axios);
originalGon = window.gon;
window.gon = Object.assign({}, dummyGon);
});
afterEach(() => {
+ mock.restore();
window.gon = originalGon;
});
@@ -38,15 +37,13 @@ describe('Api', () => {
describe('group', () => {
it('fetches a group', (done) => {
const groupId = '123456';
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}.json`;
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.dataType).toEqual('json');
- return sendDummyResponse();
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
+ mock.onGet(expectedUrl).reply(200, {
+ name: 'test',
});
Api.group(groupId, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response.name).toBe('test');
done();
});
});
@@ -57,19 +54,13 @@ describe('Api', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
- const expectedData = Object.assign({
- search: query,
- per_page: 20,
- }, options);
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.dataType).toEqual('json');
- expect(request.data).toEqual(expectedData);
- return sendDummyResponse();
- });
+ mock.onGet(expectedUrl).reply(200, [{
+ name: 'test',
+ }]);
Api.groups(query, options, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
done();
});
});
@@ -79,19 +70,13 @@ describe('Api', () => {
it('fetches namespaces', (done) => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
- const expectedData = {
- search: query,
- per_page: 20,
- };
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.dataType).toEqual('json');
- expect(request.data).toEqual(expectedData);
- return sendDummyResponse();
- });
+ mock.onGet(expectedUrl).reply(200, [{
+ name: 'test',
+ }]);
Api.namespaces(query, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
done();
});
});
@@ -103,21 +88,13 @@ describe('Api', () => {
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
window.gon.current_user_id = 1;
- const expectedData = Object.assign({
- search: query,
- per_page: 20,
- membership: true,
- simple: true,
- }, options);
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.dataType).toEqual('json');
- expect(request.data).toEqual(expectedData);
- return sendDummyResponse();
- });
+ mock.onGet(expectedUrl).reply(200, [{
+ name: 'test',
+ }]);
Api.projects(query, options, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
done();
});
});
@@ -126,20 +103,13 @@ describe('Api', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
- const expectedData = Object.assign({
- search: query,
- per_page: 20,
- simple: true,
- }, options);
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.dataType).toEqual('json');
- expect(request.data).toEqual(expectedData);
- return sendDummyResponse();
- });
+ mock.onGet(expectedUrl).reply(200, [{
+ name: 'test',
+ }]);
Api.projects(query, options, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
done();
});
});
@@ -154,16 +124,16 @@ describe('Api', () => {
const expectedData = {
label: labelData,
};
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.dataType).toEqual('json');
- expect(request.type).toEqual('POST');
- expect(request.data).toEqual(expectedData);
- return sendDummyResponse();
+ mock.onPost(expectedUrl).reply((config) => {
+ expect(config.data).toBe(JSON.stringify(expectedData));
+
+ return [200, {
+ name: 'test',
+ }];
});
Api.newLabel(namespace, project, labelData, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response.name).toBe('test');
done();
});
});
@@ -174,19 +144,13 @@ describe('Api', () => {
const groupId = '123456';
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
- const expectedData = {
- search: query,
- per_page: 20,
- };
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.dataType).toEqual('json');
- expect(request.data).toEqual(expectedData);
- return sendDummyResponse();
- });
+ mock.onGet(expectedUrl).reply(200, [{
+ name: 'test',
+ }]);
Api.groupProjects(groupId, query, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response.length).toBe(1);
+ expect(response[0].name).toBe('test');
done();
});
});
@@ -197,14 +161,10 @@ describe('Api', () => {
const licenseKey = "driver's license";
const data = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`;
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.data).toEqual(data);
- return sendDummyResponse();
- });
+ mock.onGet(expectedUrl).reply(200, 'test');
Api.licenseText(licenseKey, data, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response).toBe('test');
done();
});
});
@@ -214,13 +174,10 @@ describe('Api', () => {
it('fetches a gitignore text', (done) => {
const gitignoreKey = 'ignore git';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`;
- spyOn(jQuery, 'get').and.callFake((url, callback) => {
- expect(url).toEqual(expectedUrl);
- callback(dummyResponse);
- });
+ mock.onGet(expectedUrl).reply(200, 'test');
Api.gitignoreText(gitignoreKey, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response).toBe('test');
done();
});
});
@@ -230,13 +187,10 @@ describe('Api', () => {
it('fetches a .gitlab-ci.yml', (done) => {
const gitlabCiYmlKey = 'Y CI ML';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`;
- spyOn(jQuery, 'get').and.callFake((url, callback) => {
- expect(url).toEqual(expectedUrl);
- callback(dummyResponse);
- });
+ mock.onGet(expectedUrl).reply(200, 'test');
Api.gitlabCiYml(gitlabCiYmlKey, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response).toBe('test');
done();
});
});
@@ -246,13 +200,10 @@ describe('Api', () => {
it('fetches a Dockerfile', (done) => {
const dockerfileYmlKey = 'a giant whale';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`;
- spyOn(jQuery, 'get').and.callFake((url, callback) => {
- expect(url).toEqual(expectedUrl);
- callback(dummyResponse);
- });
+ mock.onGet(expectedUrl).reply(200, 'test');
Api.dockerfileYml(dockerfileYmlKey, (response) => {
- expect(response).toBe(dummyResponse);
+ expect(response).toBe('test');
done();
});
});
@@ -262,17 +213,13 @@ describe('Api', () => {
it('fetches an issue template', (done) => {
const namespace = 'some namespace';
const project = 'some project';
- const templateKey = 'template key';
+ const templateKey = ' template #%?.key ';
const templateType = 'template type';
- const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${templateKey}`;
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- return sendDummyResponse();
- });
+ const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`;
+ mock.onGet(expectedUrl).reply(200, 'test');
Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
- expect(error).toBe(null);
- expect(response).toBe(dummyResponse);
+ expect(response).toBe('test');
done();
});
});
@@ -283,20 +230,14 @@ describe('Api', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
- const expectedData = Object.assign({
- search: query,
- per_page: 20,
- }, options);
- spyOn(jQuery, 'ajax').and.callFake((request) => {
- expect(request.url).toEqual(expectedUrl);
- expect(request.dataType).toEqual('json');
- expect(request.data).toEqual(expectedData);
- return sendDummyResponse();
- });
+ mock.onGet(expectedUrl).reply(200, [{
+ name: 'test',
+ }]);
Api.users(query, options)
- .then((response) => {
- expect(response).toBe(dummyResponse);
+ .then(({ data }) => {
+ expect(data.length).toBe(1);
+ expect(data[0].name).toBe('test');
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/behaviors/secret_values_spec.js b/spec/javascripts/behaviors/secret_values_spec.js
index 9eeae474e7d..38d9bba6868 100644
--- a/spec/javascripts/behaviors/secret_values_spec.js
+++ b/spec/javascripts/behaviors/secret_values_spec.js
@@ -1,16 +1,24 @@
import SecretValues from '~/behaviors/secret_values';
-function generateFixtureMarkup(secrets, isRevealed) {
+function generateValueMarkup(
+ secret,
+ valueClass = 'js-secret-value',
+ placeholderClass = 'js-secret-value-placeholder',
+) {
+ return `
+ <div class="${placeholderClass}">
+ ***
+ </div>
+ <div class="hide ${valueClass}">
+ ${secret}
+ </div>
+ `;
+}
+
+function generateFixtureMarkup(secrets, isRevealed, valueClass, placeholderClass) {
return `
<div class="js-secret-container">
- ${secrets.map(secret => `
- <div class="js-secret-value-placeholder">
- ***
- </div>
- <div class="hide js-secret-value">
- ${secret}
- </div>
- `).join('')}
+ ${secrets.map(secret => generateValueMarkup(secret, valueClass, placeholderClass)).join('')}
<button
class="js-secret-value-reveal-button"
data-secret-reveal-status="${isRevealed}"
@@ -21,11 +29,25 @@ function generateFixtureMarkup(secrets, isRevealed) {
`;
}
-function setupSecretFixture(secrets, isRevealed) {
+function setupSecretFixture(
+ secrets,
+ isRevealed,
+ valueClass = 'js-secret-value',
+ placeholderClass = 'js-secret-value-placeholder',
+) {
const wrapper = document.createElement('div');
- wrapper.innerHTML = generateFixtureMarkup(secrets, isRevealed);
-
- const secretValues = new SecretValues(wrapper.querySelector('.js-secret-container'));
+ wrapper.innerHTML = generateFixtureMarkup(
+ secrets,
+ isRevealed,
+ valueClass,
+ placeholderClass,
+ );
+
+ const secretValues = new SecretValues({
+ container: wrapper.querySelector('.js-secret-container'),
+ valueSelector: `.${valueClass}`,
+ placeholderSelector: `.${placeholderClass}`,
+ });
secretValues.init();
return wrapper;
@@ -49,7 +71,7 @@ describe('setupSecretValues', () => {
expect(revealButton.textContent).toEqual('Hide value');
});
- it('should value hidden initially', () => {
+ it('should have value hidden initially', () => {
const wrapper = setupSecretFixture(secrets, false);
const values = wrapper.querySelectorAll('.js-secret-value');
const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
@@ -143,4 +165,64 @@ describe('setupSecretValues', () => {
});
});
});
+
+ describe('with dynamic secrets', () => {
+ const secrets = ['mysecret123', 'happygoat456', 'tanuki789'];
+
+ it('should toggle values and placeholders', () => {
+ const wrapper = setupSecretFixture(secrets, false);
+ // Insert the new dynamic row
+ wrapper.querySelector('.js-secret-container').insertAdjacentHTML('afterbegin', generateValueMarkup('foobarbazdynamic'));
+
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+ const values = wrapper.querySelectorAll('.js-secret-value');
+ const placeholders = wrapper.querySelectorAll('.js-secret-value-placeholder');
+
+ revealButton.click();
+
+ expect(values.length).toEqual(4);
+ values.forEach((value) => {
+ expect(value.classList.contains('hide')).toEqual(false);
+ });
+ expect(placeholders.length).toEqual(4);
+ placeholders.forEach((placeholder) => {
+ expect(placeholder.classList.contains('hide')).toEqual(true);
+ });
+
+ revealButton.click();
+
+ expect(values.length).toEqual(4);
+ values.forEach((value) => {
+ expect(value.classList.contains('hide')).toEqual(true);
+ });
+ expect(placeholders.length).toEqual(4);
+ placeholders.forEach((placeholder) => {
+ expect(placeholder.classList.contains('hide')).toEqual(false);
+ });
+ });
+ });
+
+ describe('selector options', () => {
+ const secrets = ['mysecret123'];
+
+ it('should respect `valueSelector` and `placeholderSelector` options', () => {
+ const valueClass = 'js-some-custom-placeholder-selector';
+ const placeholderClass = 'js-some-custom-value-selector';
+
+ const wrapper = setupSecretFixture(secrets, false, valueClass, placeholderClass);
+ const values = wrapper.querySelectorAll(`.${valueClass}`);
+ const placeholders = wrapper.querySelectorAll(`.${placeholderClass}`);
+ const revealButton = wrapper.querySelector('.js-secret-value-reveal-button');
+
+ expect(values.length).toEqual(1);
+ expect(placeholders.length).toEqual(1);
+
+ revealButton.click();
+
+ expect(values.length).toEqual(1);
+ expect(values[0].classList.contains('hide')).toEqual(false);
+ expect(placeholders.length).toEqual(1);
+ expect(placeholders[0].classList.contains('hide')).toEqual(true);
+ });
+ });
});
diff --git a/spec/javascripts/blob/viewer/index_spec.js b/spec/javascripts/blob/viewer/index_spec.js
index cfa6650d85f..892411a6a40 100644
--- a/spec/javascripts/blob/viewer/index_spec.js
+++ b/spec/javascripts/blob/viewer/index_spec.js
@@ -1,28 +1,35 @@
/* eslint-disable no-new */
+import MockAdapter from 'axios-mock-adapter';
import BlobViewer from '~/blob/viewer/index';
+import axios from '~/lib/utils/axios_utils';
describe('Blob viewer', () => {
let blob;
+ let mock;
+
preloadFixtures('snippets/show.html.raw');
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
loadFixtures('snippets/show.html.raw');
$('#modal-upload-blob').remove();
blob = new BlobViewer();
- spyOn($, 'ajax').and.callFake(() => {
- const d = $.Deferred();
-
- d.resolve({
- html: '<div>testing</div>',
- });
+ mock.onGet('http://test.host/snippets/1.json?viewer=rich').reply(200, {
+ html: '<div>testing</div>',
+ });
- return d.promise();
+ mock.onGet('http://test.host/snippets/1.json?viewer=simple').reply(200, {
+ html: '<div>testing</div>',
});
+
+ spyOn(axios, 'get').and.callThrough();
});
afterEach(() => {
+ mock.restore();
location.hash = '';
});
@@ -30,7 +37,6 @@ describe('Blob viewer', () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => {
- expect($.ajax).toHaveBeenCalled();
expect(
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
.classList.contains('hidden'),
@@ -46,7 +52,6 @@ describe('Blob viewer', () => {
new BlobViewer();
setTimeout(() => {
- expect($.ajax).toHaveBeenCalled();
expect(
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]')
.classList.contains('hidden'),
@@ -64,12 +69,8 @@ describe('Blob viewer', () => {
});
asyncClick()
+ .then(() => asyncClick())
.then(() => {
- expect($.ajax).toHaveBeenCalled();
- return asyncClick();
- })
- .then(() => {
- expect($.ajax.calls.count()).toBe(1);
expect(
document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
).toBe('true');
@@ -122,7 +123,6 @@ describe('Blob viewer', () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => {
- expect($.ajax).toHaveBeenCalled();
expect(
copyButton.classList.contains('disabled'),
).toBeFalsy();
@@ -135,8 +135,6 @@ describe('Blob viewer', () => {
document.querySelector('.js-blob-viewer-switch-btn[data-viewer="simple"]').click();
setTimeout(() => {
- expect($.ajax).toHaveBeenCalled();
-
expect(
copyButton.getAttribute('data-original-title'),
).toBe('Copy source to clipboard');
@@ -171,14 +169,14 @@ describe('Blob viewer', () => {
it('sends AJAX request when switching to simple view', () => {
blob.switchToViewer('simple');
- expect($.ajax).toHaveBeenCalled();
+ expect(axios.get).toHaveBeenCalled();
});
it('does not send AJAX request when switching to rich view', () => {
blob.switchToViewer('simple');
blob.switchToViewer('rich');
- expect($.ajax.calls.count()).toBe(1);
+ expect(axios.get.calls.count()).toBe(1);
});
});
});
diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
new file mode 100644
index 00000000000..0170ab458d4
--- /dev/null
+++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
@@ -0,0 +1,163 @@
+import VariableList from '~/ci_variable_list/ci_variable_list';
+import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+
+describe('VariableList', () => {
+ preloadFixtures('pipeline_schedules/edit.html.raw');
+ preloadFixtures('pipeline_schedules/edit_with_variables.html.raw');
+
+ let $wrapper;
+ let variableList;
+
+ describe('with only key/value inputs', () => {
+ describe('with no variables', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html.raw');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'schedule',
+ });
+ variableList.init();
+ });
+
+ it('should remove the row when clicking the remove button', () => {
+ $wrapper.find('.js-row-remove-button').trigger('click');
+
+ expect($wrapper.find('.js-row').length).toBe(0);
+ });
+
+ it('should add another row when editing the last rows key input', () => {
+ const $row = $wrapper.find('.js-row');
+ $row.find('.js-ci-variable-input-key')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ // Check for the correct default in the new row
+ const $keyInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
+ expect($keyInput.val()).toBe('');
+ });
+
+ it('should add another row when editing the last rows value textarea', () => {
+ const $row = $wrapper.find('.js-row');
+ $row.find('.js-ci-variable-input-value')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ // Check for the correct default in the new row
+ const $valueInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-key');
+ expect($valueInput.val()).toBe('');
+ });
+
+ it('should remove empty row after blurring', () => {
+ const $row = $wrapper.find('.js-row');
+ $row.find('.js-ci-variable-input-key')
+ .val('foo')
+ .trigger('input');
+
+ expect($wrapper.find('.js-row').length).toBe(2);
+
+ $row.find('.js-ci-variable-input-key')
+ .val('')
+ .trigger('input')
+ .trigger('blur');
+
+ expect($wrapper.find('.js-row').length).toBe(1);
+ });
+ });
+
+ describe('with persisted variables', () => {
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit_with_variables.html.raw');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'schedule',
+ });
+ variableList.init();
+ });
+
+ it('should have "Reveal values" button initially when there are already variables', () => {
+ expect($wrapper.find('.js-secret-value-reveal-button').text()).toBe('Reveal values');
+ });
+
+ it('should reveal hidden values', () => {
+ const $row = $wrapper.find('.js-row:first-child');
+ const $inputValue = $row.find('.js-ci-variable-input-value');
+ const $placeholder = $row.find('.js-secret-value-placeholder');
+
+ expect($placeholder.hasClass('hide')).toBe(false);
+ expect($inputValue.hasClass('hide')).toBe(true);
+
+ // Reveal values
+ $wrapper.find('.js-secret-value-reveal-button').click();
+
+ expect($placeholder.hasClass('hide')).toBe(true);
+ expect($inputValue.hasClass('hide')).toBe(false);
+ });
+ });
+ });
+
+ describe('with all inputs(key, value, protected)', () => {
+ beforeEach(() => {
+ // This markup will be replaced with a fixture when we can render the
+ // CI/CD settings page with the new dynamic variable list in https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4110
+ $wrapper = $(`<form class="js-variable-list">
+ <ul>
+ <li class="js-row">
+ <div class="ci-variable-body-item">
+ <input class="js-ci-variable-input-key" name="variables[variables_attributes][][key]">
+ </div>
+
+ <div class="ci-variable-body-item">
+ <textarea class="js-ci-variable-input-value" name="variables[variables_attributes][][value]"></textarea>
+ </div>
+
+ <div class="ci-variable-body-item ci-variable-protected-item">
+ <button type="button" class="js-project-feature-toggle project-feature-toggle">
+ <input
+ type="hidden"
+ class="js-ci-variable-input-protected js-project-feature-toggle-input"
+ name="variables[variables_attributes][][protected]"
+ value="true"
+ />
+ </button>
+ </div>
+
+ <button type="button" class="js-row-remove-button"></button>
+ </li>
+ </ul>
+ <button type="button" class="js-secret-value-reveal-button">
+ Reveal values
+ </button>
+ </form>`);
+
+ variableList = new VariableList({
+ container: $wrapper,
+ formField: 'variables',
+ });
+ variableList.init();
+ });
+
+ it('should 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);
+
+ // Check for the correct default in the new row
+ const $protectedInput = $wrapper.find('.js-row:last-child').find('.js-ci-variable-input-protected');
+ expect($protectedInput.val()).toBe('true');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
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
new file mode 100644
index 00000000000..eb508a7f059
--- /dev/null
+++ b/spec/javascripts/ci_variable_list/native_form_variable_list_spec.js
@@ -0,0 +1,30 @@
+import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
+
+describe('NativeFormVariableList', () => {
+ preloadFixtures('pipeline_schedules/edit.html.raw');
+
+ let $wrapper;
+
+ beforeEach(() => {
+ loadFixtures('pipeline_schedules/edit.html.raw');
+ $wrapper = $('.js-ci-variable-list-section');
+
+ setupNativeFormVariableList({
+ container: $wrapper,
+ formField: 'schedule',
+ });
+ });
+
+ describe('onFormSubmit', () => {
+ it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
+ const $row = $wrapper.find('.js-row');
+ expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('schedule[variables_attributes][][key]');
+ expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('schedule[variables_attributes][][value]');
+
+ $wrapper.closest('form').trigger('trigger-submit');
+
+ expect($row.find('.js-ci-variable-input-key').attr('name')).toBe('');
+ expect($row.find('.js-ci-variable-input-value').attr('name')).toBe('');
+ });
+ });
+});
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index f5be9ea0fb2..7b38f6b7855 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -23,16 +23,24 @@ describe('Clusters', () => {
});
describe('toggle', () => {
- it('should update the button and the input field on click', () => {
- cluster.toggleButton.click();
+ 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');
- expect(
- cluster.toggleButton.classList,
- ).not.toContain('is-checked');
+ toggleButton.click();
- expect(
- cluster.toggleInput.getAttribute('value'),
- ).toEqual('false');
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(
+ toggleButton.classList,
+ ).not.toContain('is-checked');
+
+ expect(
+ toggleInput.getAttribute('value'),
+ ).toEqual('false');
+ })
+ .then(done)
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js
deleted file mode 100644
index 0a8b63ed5b4..00000000000
--- a/spec/javascripts/clusters/clusters_index_spec.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import setClusterTableToggles from '~/clusters/clusters_index';
-import { setTimeout } from 'core-js/library/web/timers';
-
-describe('Clusters table', () => {
- preloadFixtures('clusters/index_cluster.html.raw');
- let mock;
-
- beforeEach(() => {
- loadFixtures('clusters/index_cluster.html.raw');
- mock = new MockAdapter(axios);
- setClusterTableToggles();
- });
-
- describe('update cluster', () => {
- it('renders loading state while request is made', () => {
- const button = document.querySelector('.js-toggle-cluster-list');
-
- button.click();
-
- expect(button.classList).toContain('is-loading');
- expect(button.getAttribute('disabled')).toEqual('true');
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('shows updated state after sucessfull request', (done) => {
- mock.onPut().reply(200, {}, {});
- const button = document.querySelector('.js-toggle-cluster-list');
- button.click();
-
- expect(button.classList).toContain('is-loading');
-
- setTimeout(() => {
- expect(button.classList).not.toContain('is-loading');
- expect(button.classList).not.toContain('is-checked');
- done();
- }, 0);
- });
-
- it('shows inital state after failed request', (done) => {
- mock.onPut().reply(500, {}, {});
- const button = document.querySelector('.js-toggle-cluster-list');
-
- button.click();
- expect(button.classList).toContain('is-loading');
-
- setTimeout(() => {
- expect(button.classList).not.toContain('is-loading');
- expect(button.classList).toContain('is-checked');
- done();
- }, 0);
- });
- });
-});
diff --git a/spec/javascripts/collapsed_sidebar_todo_spec.js b/spec/javascripts/collapsed_sidebar_todo_spec.js
index 5026eaafaca..2abf52a1676 100644
--- a/spec/javascripts/collapsed_sidebar_todo_spec.js
+++ b/spec/javascripts/collapsed_sidebar_todo_spec.js
@@ -1,10 +1,14 @@
/* eslint-disable no-new */
import _ from 'underscore';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
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 jsonFixtureName = 'todos/todos.json';
+ let mock;
preloadFixtures(fixtureName);
preloadFixtures(jsonFixtureName);
@@ -19,19 +23,26 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
document.querySelector('.js-right-sidebar')
.classList.toggle('right-sidebar-collapsed');
- spyOn(jQuery, 'ajax').and.callFake((res) => {
- const d = $.Deferred();
+ mock = new MockAdapter(axios);
+
+ mock.onPost(`${gl.TEST_HOST}/frontend-fixtures/issues-project/todos`).reply(() => {
const response = _.clone(todoData);
- if (res.type === 'DELETE') {
- delete response.delete_path;
- }
+ return [200, response];
+ });
- d.resolve(response);
- return d.promise();
+ mock.onDelete(/(.*)\/dashboard\/todos\/\d+$/).reply(() => {
+ const response = _.clone(todoData);
+ delete response.delete_path;
+
+ return [200, response];
});
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('shows add todo button', () => {
expect(
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon'),
@@ -52,71 +63,101 @@ describe('Issuable right sidebar collapsed todo toggle', () => {
).toBe('Add todo');
});
- it('toggle todo state', () => {
+ it('toggle todo state', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).not.toBeNull();
+ setTimeout(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).not.toBeNull();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'),
- ).not.toBeNull();
- });
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .fa-check-square'),
+ ).not.toBeNull();
- it('toggle todo state of expanded todo toggle', () => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
-
- expect(
- document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Mark done');
+ done();
+ });
});
- it('toggles todo button tooltip', () => {
+ it('toggle todo state of expanded todo toggle', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
- ).toBe('Mark done');
- });
-
- it('marks todo as done', () => {
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+ setTimeout(() => {
+ expect(
+ document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
+ ).toBe('Mark done');
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).not.toBeNull();
+ done();
+ });
+ });
+ it('toggles todo button tooltip', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
- ).toBeNull();
+ setTimeout(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('data-original-title'),
+ ).toBe('Mark done');
- expect(
- document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
- ).toBe('Add todo');
+ done();
+ });
});
- it('updates aria-label to mark done', () => {
+ it('marks todo as done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ timeoutPromise()
+ .then(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).not.toBeNull();
+
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+ })
+ .then(timeoutPromise)
+ .then(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon .todo-undone'),
+ ).toBeNull();
+
+ expect(
+ document.querySelector('.issuable-sidebar-header .js-issuable-todo').textContent.trim(),
+ ).toBe('Add todo');
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('updates aria-label to add todo', () => {
+ it('updates aria-label to mark done', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Mark done');
+ setTimeout(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Mark done');
+ done();
+ });
+ });
+
+ it('updates aria-label to add todo', (done) => {
document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
- expect(
- document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
- ).toBe('Add todo');
+ timeoutPromise()
+ .then(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Mark done');
+
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').click();
+ })
+ .then(timeoutPromise)
+ .then(() => {
+ expect(
+ document.querySelector('.js-issuable-todo.sidebar-collapsed-icon').getAttribute('aria-label'),
+ ).toBe('Add todo');
+ })
+ .then(done)
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
new file mode 100644
index 00000000000..90f290e845e
--- /dev/null
+++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
@@ -0,0 +1,104 @@
+import Vue from 'vue';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Commit pipeline status component', () => {
+ let vm;
+ let Component;
+ let mock;
+ const mockCiStatus = {
+ details_path: '/root/hello-world/pipelines/1',
+ favicon: 'canceled.ico',
+ group: 'canceled',
+ has_details: true,
+ icon: 'status_canceled',
+ label: 'canceled',
+ text: 'canceled',
+ };
+
+ beforeEach(() => {
+ Component = Vue.extend(commitPipelineStatus);
+ });
+
+ describe('While polling pipeline data succesfully', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/dummy/endpoint').reply(() => {
+ const res = Promise.resolve([200, {
+ pipelines: [
+ {
+ details: {
+ status: mockCiStatus,
+ },
+ },
+ ],
+ }]);
+ return res;
+ });
+ vm = mountComponent(Component, {
+ endpoint: '/dummy/endpoint',
+ });
+ });
+
+ afterEach(() => {
+ vm.poll.stop();
+ vm.$destroy();
+ mock.restore();
+ });
+
+ it('shows the loading icon when polling is starting', (done) => {
+ expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.loading-container')).toBe(null);
+ done();
+ });
+ });
+
+ it('contains a ciStatus when the polling is succesful ', (done) => {
+ setTimeout(() => {
+ expect(vm.ciStatus).toEqual(mockCiStatus);
+ done();
+ });
+ });
+
+ it('contains a ci-status icon when polling is succesful', (done) => {
+ setTimeout(() => {
+ expect(vm.$el.querySelector('.ci-status-icon')).not.toBe(null);
+ expect(vm.$el.querySelector('.ci-status-icon').classList).toContain(`ci-status-icon-${mockCiStatus.group}`);
+ done();
+ });
+ });
+ });
+
+ describe('When polling data was not succesful', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onGet('/dummy/endpoint').reply(() => {
+ const res = Promise.reject([502, { }]);
+ return res;
+ });
+ vm = new Component({
+ props: {
+ endpoint: '/dummy/endpoint',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.poll.stop();
+ vm.$destroy();
+ mock.restore();
+ });
+
+ it('calls an errorCallback', (done) => {
+ spyOn(vm, 'errorCallback').and.callThrough();
+ vm.$mount();
+ setTimeout(() => {
+ expect(vm.errorCallback.calls.count()).toEqual(1);
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index d62c2966a8b..0afe09d87bc 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -10,9 +10,10 @@ describe('Pipelines table in Commits and Merge requests', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
- PipelinesTable = Vue.extend(pipelinesTable);
const pipelines = getJSONFixture(jsonFixtureName).pipelines;
- pipeline = pipelines.find(p => p.id === 1);
+
+ PipelinesTable = Vue.extend(pipelinesTable);
+ pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
});
describe('successful request', () => {
diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js
index d0176520440..44ec9e4eabf 100644
--- a/spec/javascripts/commits_spec.js
+++ b/spec/javascripts/commits_spec.js
@@ -1,4 +1,6 @@
import 'vendor/jquery.endless-scroll';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import CommitsList from '~/commits';
describe('Commits List', () => {
@@ -43,30 +45,47 @@ describe('Commits List', () => {
describe('on entering input', () => {
let ajaxSpy;
+ let mock;
beforeEach(() => {
CommitsList.init(25);
CommitsList.searchField.val('');
spyOn(history, 'replaceState').and.stub();
- ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => {
- req.success({
- data: '<li>Result</li>',
- });
+ mock = new MockAdapter(axios);
+
+ mock.onGet('/h5bp/html5-boilerplate/commits/master').reply(200, {
+ html: '<li>Result</li>',
});
+
+ ajaxSpy = spyOn(axios, 'get').and.callThrough();
+ });
+
+ afterEach(() => {
+ mock.restore();
});
- it('should save the last search string', () => {
+ it('should save the last search string', (done) => {
CommitsList.searchField.val('GitLab');
- CommitsList.filterResults();
- expect(ajaxSpy).toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('GitLab');
+ CommitsList.filterResults()
+ .then(() => {
+ expect(ajaxSpy).toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('GitLab');
+
+ done();
+ })
+ .catch(done.fail);
});
- it('should not make ajax call if the input does not change', () => {
- CommitsList.filterResults();
- expect(ajaxSpy).not.toHaveBeenCalled();
- expect(CommitsList.lastSearch).toEqual('');
+ it('should not make ajax call if the input does not change', (done) => {
+ CommitsList.filterResults()
+ .then(() => {
+ expect(ajaxSpy).not.toHaveBeenCalled();
+ expect(CommitsList.lastSearch).toEqual('');
+
+ done();
+ })
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/create_item_dropdown_spec.js b/spec/javascripts/create_item_dropdown_spec.js
index c8b00a4f553..143137c23ec 100644
--- a/spec/javascripts/create_item_dropdown_spec.js
+++ b/spec/javascripts/create_item_dropdown_spec.js
@@ -18,54 +18,67 @@ describe('CreateItemDropdown', () => {
preloadFixtures('static/create_item_dropdown.html.raw');
let $wrapperEl;
+ let createItemDropdown;
+
+ function createItemAndClearInput(text) {
+ // Filter for the new item
+ $wrapperEl.find('.dropdown-input-field')
+ .val(text)
+ .trigger('input');
+
+ // Create the new item
+ const $createButton = $wrapperEl.find('.js-dropdown-create-new-item');
+ $createButton.click();
+
+ // Clear out the filter
+ $wrapperEl.find('.dropdown-input-field')
+ .val('')
+ .trigger('input');
+ }
beforeEach(() => {
loadFixtures('static/create_item_dropdown.html.raw');
$wrapperEl = $('.js-create-item-dropdown-fixture-root');
-
- // eslint-disable-next-line no-new
- new CreateItemDropdown({
- $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
- defaultToggleLabel: 'All variables',
- fieldName: 'variable[environment]',
- getData: (term, callback) => {
- callback(DROPDOWN_ITEM_DATA);
- },
- });
});
afterEach(() => {
$wrapperEl.remove();
});
- it('should have a dropdown item for each piece of data', () => {
- // Get the data in the dropdown
- $('.js-dropdown-menu-toggle').click();
+ describe('items', () => {
+ beforeEach(() => {
+ createItemDropdown = new CreateItemDropdown({
+ $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
+ defaultToggleLabel: 'All variables',
+ fieldName: 'variable[environment]',
+ getData: (term, callback) => {
+ callback(DROPDOWN_ITEM_DATA);
+ },
+ });
+ });
+
+ it('should have a dropdown item for each piece of data', () => {
+ // Get the data in the dropdown
+ $('.js-dropdown-menu-toggle').click();
- const $itemEls = $wrapperEl.find('.js-dropdown-content a');
- expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
+ const $itemEls = $wrapperEl.find('.js-dropdown-content a');
+ expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
+ });
});
describe('created items', () => {
const NEW_ITEM_TEXT = 'foobarbaz';
- function createItemAndClearInput(text) {
- // Filter for the new item
- $wrapperEl.find('.dropdown-input-field')
- .val(text)
- .trigger('input');
-
- // Create the new item
- const $createButton = $wrapperEl.find('.js-dropdown-create-new-item');
- $createButton.click();
-
- // Clear out the filter
- $wrapperEl.find('.dropdown-input-field')
- .val('')
- .trigger('input');
- }
-
beforeEach(() => {
+ createItemDropdown = new CreateItemDropdown({
+ $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
+ defaultToggleLabel: 'All variables',
+ fieldName: 'variable[environment]',
+ getData: (term, callback) => {
+ callback(DROPDOWN_ITEM_DATA);
+ },
+ });
+
// Open the dropdown
$('.js-dropdown-menu-toggle').click();
@@ -103,4 +116,68 @@ describe('CreateItemDropdown', () => {
expect($itemEls.length).toEqual(DROPDOWN_ITEM_DATA.length);
});
});
+
+ describe('clearDropdown()', () => {
+ beforeEach(() => {
+ createItemDropdown = new CreateItemDropdown({
+ $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
+ defaultToggleLabel: 'All variables',
+ fieldName: 'variable[environment]',
+ getData: (term, callback) => {
+ callback(DROPDOWN_ITEM_DATA);
+ },
+ });
+ });
+
+ it('should clear all data and filter input', () => {
+ const filterInput = $wrapperEl.find('.dropdown-input-field');
+
+ // Get the data in the dropdown
+ $('.js-dropdown-menu-toggle').click();
+
+ // Filter for an item
+ filterInput
+ .val('one')
+ .trigger('input');
+
+ const $itemElsAfterFilter = $wrapperEl.find('.js-dropdown-content a');
+ expect($itemElsAfterFilter.length).toEqual(1);
+
+ createItemDropdown.clearDropdown();
+
+ const $itemElsAfterClear = $wrapperEl.find('.js-dropdown-content a');
+ expect($itemElsAfterClear.length).toEqual(0);
+ expect(filterInput.val()).toEqual('');
+ });
+ });
+
+ describe('createNewItemFromValue option', () => {
+ beforeEach(() => {
+ createItemDropdown = new CreateItemDropdown({
+ $dropdown: $wrapperEl.find('.js-dropdown-menu-toggle'),
+ defaultToggleLabel: 'All variables',
+ fieldName: 'variable[environment]',
+ getData: (term, callback) => {
+ callback(DROPDOWN_ITEM_DATA);
+ },
+ createNewItemFromValue: newValue => ({
+ title: `${newValue}-title`,
+ id: `${newValue}-id`,
+ text: `${newValue}-text`,
+ }),
+ });
+ });
+
+ it('all items go through createNewItemFromValue', () => {
+ // Get the data in the dropdown
+ $('.js-dropdown-menu-toggle').click();
+
+ createItemAndClearInput('new-item');
+
+ const $itemEls = $wrapperEl.find('.js-dropdown-content a');
+ expect($itemEls.length).toEqual(1 + DROPDOWN_ITEM_DATA.length);
+ expect($($itemEls[3]).text()).toEqual('new-item-text');
+ expect($wrapperEl.find('.dropdown-toggle-text').text()).toEqual('new-item-title');
+ });
+ });
});
diff --git a/spec/javascripts/environments/environment_item_spec.js b/spec/javascripts/environments/environment_item_spec.js
index 0e141adb628..7a34126eef7 100644
--- a/spec/javascripts/environments/environment_item_spec.js
+++ b/spec/javascripts/environments/environment_item_spec.js
@@ -68,7 +68,7 @@ describe('Environment item', () => {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
@@ -84,7 +84,7 @@ describe('Environment item', () => {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
new file mode 100644
index 00000000000..34ffc7b1016
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
@@ -0,0 +1,231 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import {
+ getSelector,
+ togglePopover,
+ dismiss,
+ mouseleave,
+ mouseenter,
+ inserted,
+} from '~/feature_highlight/feature_highlight_helper';
+import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+
+describe('feature highlight helper', () => {
+ describe('getSelector', () => {
+ it('returns js-feature-highlight selector', () => {
+ const highlightId = 'highlightId';
+ expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`);
+ });
+ });
+
+ describe('togglePopover', () => {
+ describe('togglePopover(true)', () => {
+ it('returns true when popover is shown', () => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ expect(togglePopover.call(context, true)).toEqual(true);
+ });
+
+ it('returns false when popover is already shown', () => {
+ const context = {
+ hasClass: () => true,
+ };
+
+ expect(togglePopover.call(context, true)).toEqual(false);
+ });
+
+ it('shows popover', (done) => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'popover').and.callFake((method) => {
+ expect(method).toEqual('show');
+ done();
+ });
+
+ togglePopover.call(context, true);
+ });
+
+ it('adds disable-animation and js-popover-show class', (done) => {
+ const context = {
+ hasClass: () => false,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
+ expect(classNames).toEqual('disable-animation js-popover-show');
+ expect(show).toEqual(true);
+ done();
+ });
+
+ togglePopover.call(context, true);
+ });
+ });
+
+ describe('togglePopover(false)', () => {
+ it('returns true when popover is hidden', () => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ expect(togglePopover.call(context, false)).toEqual(true);
+ });
+
+ it('returns false when popover is already hidden', () => {
+ const context = {
+ hasClass: () => false,
+ };
+
+ expect(togglePopover.call(context, false)).toEqual(false);
+ });
+
+ it('hides popover', (done) => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'popover').and.callFake((method) => {
+ expect(method).toEqual('hide');
+ done();
+ });
+
+ togglePopover.call(context, false);
+ });
+
+ it('removes disable-animation and js-popover-show class', (done) => {
+ const context = {
+ hasClass: () => true,
+ popover: () => {},
+ toggleClass: () => {},
+ };
+
+ spyOn(context, 'toggleClass').and.callFake((classNames, show) => {
+ expect(classNames).toEqual('disable-animation js-popover-show');
+ expect(show).toEqual(false);
+ done();
+ });
+
+ togglePopover.call(context, false);
+ });
+ });
+ });
+
+ describe('dismiss', () => {
+ let mock;
+ const context = {
+ hide: () => {},
+ attr: () => '/-/callouts/dismiss',
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ spyOn(togglePopover, 'call').and.callFake(() => {});
+ spyOn(context, 'hide').and.callFake(() => {});
+ dismiss.call(context);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('calls persistent dismissal endpoint', (done) => {
+ const spy = jasmine.createSpy('dismiss-endpoint-hit');
+ mock.onPost('/-/callouts/dismiss').reply(spy);
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(spy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('calls hide popover', () => {
+ expect(togglePopover.call).toHaveBeenCalledWith(context, false);
+ });
+
+ it('calls hide', () => {
+ expect(context.hide).toHaveBeenCalled();
+ });
+ });
+
+ describe('mouseleave', () => {
+ it('calls hide popover if .popover:hover is false', () => {
+ const fakeJquery = {
+ length: 0,
+ };
+
+ spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
+ spyOn(togglePopover, 'call');
+ mouseleave();
+ expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false);
+ });
+
+ it('does not call hide popover if .popover:hover is true', () => {
+ const fakeJquery = {
+ length: 1,
+ };
+
+ spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn));
+ spyOn(togglePopover, 'call');
+ mouseleave();
+ expect(togglePopover.call).not.toHaveBeenCalledWith(false);
+ });
+ });
+
+ describe('mouseenter', () => {
+ const context = {};
+
+ it('shows popover', () => {
+ spyOn(togglePopover, 'call').and.returnValue(false);
+ mouseenter.call(context);
+ expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true);
+ });
+
+ it('registers mouseleave event if popover is showed', (done) => {
+ spyOn(togglePopover, 'call').and.returnValue(true);
+ spyOn($.fn, 'on').and.callFake((eventName) => {
+ expect(eventName).toEqual('mouseleave');
+ done();
+ });
+ mouseenter.call(context);
+ });
+
+ it('does not register mouseleave event if popover is not showed', () => {
+ spyOn(togglePopover, 'call').and.returnValue(false);
+ const spy = spyOn($.fn, 'on').and.callFake(() => {});
+ mouseenter.call(context);
+ expect(spy).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('inserted', () => {
+ it('registers click event callback', (done) => {
+ const context = {
+ getAttribute: () => 'popoverId',
+ dataset: {
+ highlight: 'some-feature',
+ },
+ };
+
+ spyOn($.fn, 'on').and.callFake((event) => {
+ expect(event).toEqual('click');
+ done();
+ });
+ inserted.call(context);
+ });
+ });
+});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
new file mode 100644
index 00000000000..7f9425d8abe
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js
@@ -0,0 +1,30 @@
+import domContentLoaded from '~/feature_highlight/feature_highlight_options';
+import bp from '~/breakpoints';
+
+describe('feature highlight options', () => {
+ describe('domContentLoaded', () => {
+ it('should not call highlightFeatures when breakpoint is xs', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('xs');
+
+ expect(domContentLoaded()).toBe(false);
+ });
+
+ it('should not call highlightFeatures when breakpoint is sm', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('sm');
+
+ expect(domContentLoaded()).toBe(false);
+ });
+
+ it('should not call highlightFeatures when breakpoint is md', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('md');
+
+ expect(domContentLoaded()).toBe(false);
+ });
+
+ it('should call highlightFeatures when breakpoint is lg', () => {
+ spyOn(bp, 'getBreakpointSize').and.returnValue('lg');
+
+ expect(domContentLoaded()).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js
new file mode 100644
index 00000000000..6e1b0429ab7
--- /dev/null
+++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js
@@ -0,0 +1,131 @@
+import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper';
+import * as featureHighlight from '~/feature_highlight/feature_highlight';
+
+describe('feature highlight', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div>
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled>
+ Trigger
+ </div>
+ </div>
+ <div class="feature-highlight-popover-content">
+ Content
+ <div class="dismiss-feature-highlight">
+ Dismiss
+ </div>
+ </div>
+ `);
+ });
+
+ describe('setupFeatureHighlightPopover', () => {
+ const selector = '.js-feature-highlight[data-highlight=test]';
+ beforeEach(() => {
+ spyOn(window, 'addEventListener');
+ spyOn(window, 'removeEventListener');
+ featureHighlight.setupFeatureHighlightPopover('test', 0);
+ });
+
+ it('setup popover content', () => {
+ const $popoverContent = $('.feature-highlight-popover-content');
+ const outerHTML = $popoverContent.prop('outerHTML');
+
+ expect($(selector).data('content')).toEqual(outerHTML);
+ });
+
+ it('setup mouseenter', () => {
+ const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
+ $(selector).trigger('mouseenter');
+
+ expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true);
+ });
+
+ it('setup debounced mouseleave', (done) => {
+ const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call');
+ $(selector).trigger('mouseleave');
+
+ // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce
+ setTimeout(() => {
+ expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false);
+ done();
+ }, 0);
+ });
+
+ it('setup inserted.bs.popover', () => {
+ $(selector).trigger('mouseenter');
+ const popoverId = $(selector).attr('aria-describedby');
+ const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click');
+
+ $(`#${popoverId} .dismiss-feature-highlight`).click();
+ expect(spyEvent).toHaveBeenTriggered();
+ });
+
+ it('setup show.bs.popover', () => {
+ $(selector).trigger('show.bs.popover');
+ expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
+ });
+
+ it('setup hide.bs.popover', () => {
+ $(selector).trigger('hide.bs.popover');
+ expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function));
+ });
+
+ it('removes disabled attribute', () => {
+ expect($('.js-feature-highlight').is(':disabled')).toEqual(false);
+ });
+
+ it('displays popover', () => {
+ expect($(selector).attr('aria-describedby')).toBeFalsy();
+ $(selector).trigger('mouseenter');
+ expect($(selector).attr('aria-describedby')).toBeTruthy();
+ });
+ });
+
+ describe('findHighestPriorityFeature', () => {
+ beforeEach(() => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+ });
+
+ it('should pick the highest priority feature highlight', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+
+ expect($('.js-feature-highlight').length).toBeGreaterThan(1);
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
+ });
+
+ it('should work when no priority is set', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test" disabled></div>
+ `);
+
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test');
+ });
+
+ it('should pick the highest priority feature highlight when some have no priority set', () => {
+ setFixtures(`
+ <div class="js-feature-highlight" data-highlight="test-no-priority1" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test" data-highlight-priority="10" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-no-priority2" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-high-priority" data-highlight-priority="20" disabled></div>
+ <div class="js-feature-highlight" data-highlight="test-low-priority" data-highlight-priority="0" disabled></div>
+ `);
+
+ expect($('.js-feature-highlight').length).toBeGreaterThan(1);
+ expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority');
+ });
+ });
+
+ describe('highlightFeatures', () => {
+ it('calls setupFeatureHighlightPopover', () => {
+ expect(featureHighlight.highlightFeatures()).toEqual('test');
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index d26ea3febe8..8e74c4f859c 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -31,19 +31,4 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
-
- context 'rendering non-empty state' do
- before do
- cluster
- end
-
- it 'clusters/index_cluster.html.raw' do |example|
- get :index,
- namespace_id: namespace,
- project_id: project
-
- expect(response).to be_success
- store_frontend_fixture(response, example.description)
- end
- end
end
diff --git a/spec/javascripts/fixtures/pipeline_schedules.rb b/spec/javascripts/fixtures/pipeline_schedules.rb
new file mode 100644
index 00000000000..56f27ea7df1
--- /dev/null
+++ b/spec/javascripts/fixtures/pipeline_schedules.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Projects::PipelineSchedulesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, :public, :repository) }
+ let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: admin) }
+ let!(:pipeline_schedule_populated) { create(:ci_pipeline_schedule, project: project, owner: admin) }
+ let!(:pipeline_schedule_variable1) { create(:ci_pipeline_schedule_variable, key: 'foo', value: 'foovalue', pipeline_schedule: pipeline_schedule_populated) }
+ let!(:pipeline_schedule_variable2) { create(:ci_pipeline_schedule_variable, key: 'bar', value: 'barvalue', pipeline_schedule: pipeline_schedule_populated) }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('pipeline_schedules/')
+ end
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'pipeline_schedules/edit.html.raw' do |example|
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: pipeline_schedule.id
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+
+ it 'pipeline_schedules/edit_with_variables.html.raw' do |example|
+ get :edit,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: pipeline_schedule_populated.id
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 1339ee00870..68a150f602a 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -14,7 +14,7 @@
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon",
"web_url": "http://localhost:3000/u/root"
},
"name": "test",
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index 6f357306ec7..50a587ef351 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -130,16 +130,25 @@ describe('GfmAutoComplete', function () {
});
describe('should not match special sequences', () => {
- const ShouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
+ const shouldNotBeFollowedBy = flags.concat(['\x00', '\x10', '\x3f', '\n', ' ']);
+ const shouldNotBePrependedBy = ['`'];
flagsUseDefaultMatcher.forEach((atSign) => {
- ShouldNotBeFollowedBy.forEach((followedSymbol) => {
+ shouldNotBeFollowedBy.forEach((followedSymbol) => {
const seq = atSign + followedSymbol;
it(`should not match "${seq}"`, () => {
expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
});
});
+
+ shouldNotBePrependedBy.forEach((prependedSymbol) => {
+ const seq = prependedSymbol + atSign;
+
+ it(`should not match "${seq}"`, () => {
+ expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null);
+ });
+ });
});
});
});
diff --git a/spec/javascripts/helpers/user_mock_data_helper.js b/spec/javascripts/helpers/user_mock_data_helper.js
index a9783ea065c..323fee3767e 100644
--- a/spec/javascripts/helpers/user_mock_data_helper.js
+++ b/spec/javascripts/helpers/user_mock_data_helper.js
@@ -4,7 +4,7 @@ export default {
for (let i = 0; i < numberUsers; i = i += 1) {
users.push(
{
- avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: (i + 1),
name: `GitLab User ${i}`,
username: `gitlab${i}`,
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
index 9033eb9ce02..d0fba908e34 100644
--- a/spec/javascripts/integrations/integration_settings_form_spec.js
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -1,3 +1,5 @@
+import MockAdaptor from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
describe('IntegrationSettingsForm', () => {
@@ -109,91 +111,117 @@ describe('IntegrationSettingsForm', () => {
describe('testSettings', () => {
let integrationSettingsForm;
let formData;
+ let mock;
beforeEach(() => {
+ mock = new MockAdaptor(axios);
+
+ spyOn(axios, 'put').and.callThrough();
+
integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
formData = integrationSettingsForm.$form.serialize();
});
- it('should make an ajax request with provided `formData`', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ afterEach(() => {
+ mock.restore();
+ });
- integrationSettingsForm.testSettings(formData);
+ it('should make an ajax request with provided `formData`', (done) => {
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
- expect($.ajax).toHaveBeenCalledWith({
- type: 'PUT',
- url: integrationSettingsForm.testEndPoint,
- data: formData,
- });
+ done();
+ })
+ .catch(done.fail);
});
- it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => {
+ it('should show error Flash with `Save anyway` action if ajax request responds with error in test', (done) => {
const errorMessage = 'Test failed.';
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
-
- integrationSettingsForm.testSettings(formData);
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ service_response: 'some error',
+ });
- deferred.resolve({ error: true, message: errorMessage, service_response: 'some error' });
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ const $flashContainer = $('.flash-container');
+ expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error');
+ expect($flashContainer.find('.flash-action')).toBeDefined();
+ expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
- const $flashContainer = $('.flash-container');
- expect($flashContainer.find('.flash-text').text().trim()).toEqual('Test failed. some error');
- expect($flashContainer.find('.flash-action')).toBeDefined();
- expect($flashContainer.find('.flash-action').text().trim()).toEqual('Save anyway');
+ done();
+ })
+ .catch(done.fail);
});
- it('should submit form if ajax request responds without any error in test', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should submit form if ajax request responds without any error in test', (done) => {
+ spyOn(integrationSettingsForm.$form, 'submit');
- integrationSettingsForm.testSettings(formData);
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ });
- spyOn(integrationSettingsForm.$form, 'submit');
- deferred.resolve({ error: false });
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
- expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
});
- it('should submit form when clicked on `Save anyway` action of error Flash', () => {
- const errorMessage = 'Test failed.';
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should submit form when clicked on `Save anyway` action of error Flash', (done) => {
+ spyOn(integrationSettingsForm.$form, 'submit');
- integrationSettingsForm.testSettings(formData);
+ const errorMessage = 'Test failed.';
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: errorMessage,
+ });
- deferred.resolve({ error: true, message: errorMessage });
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ const $flashAction = $('.flash-container .flash-action');
+ expect($flashAction).toBeDefined();
- const $flashAction = $('.flash-container .flash-action');
- expect($flashAction).toBeDefined();
+ $flashAction.get(0).click();
+ })
+ .then(() => {
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
- spyOn(integrationSettingsForm.$form, 'submit');
- $flashAction.get(0).click();
- expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ done();
+ })
+ .catch(done.fail);
});
- it('should show error Flash if ajax request failed', () => {
+ it('should show error Flash if ajax request failed', (done) => {
const errorMessage = 'Something went wrong on our end.';
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- integrationSettingsForm.testSettings(formData);
+ mock.onPut(integrationSettingsForm.testEndPoint).networkError();
- deferred.reject();
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage);
- expect($('.flash-container .flash-text').text().trim()).toEqual(errorMessage);
+ done();
+ })
+ .catch(done.fail);
});
- it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
-
- integrationSettingsForm.testSettings(formData);
+ it('should always call `toggleSubmitBtnState` with `false` once request is completed', (done) => {
+ mock.onPut(integrationSettingsForm.testEndPoint).networkError();
spyOn(integrationSettingsForm, 'toggleSubmitBtnState');
- deferred.reject();
- expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
+ integrationSettingsForm.testSettings(formData)
+ .then(() => {
+ expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
+
+ done();
+ })
+ .catch(done.fail);
});
});
});
diff --git a/spec/javascripts/issuable_spec.js b/spec/javascripts/issuable_spec.js
index 5a9112716f4..d53ffecbd35 100644
--- a/spec/javascripts/issuable_spec.js
+++ b/spec/javascripts/issuable_spec.js
@@ -1,3 +1,5 @@
+import MockAdaptor from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import IssuableIndex from '~/issuable_index';
describe('Issuable', () => {
@@ -19,6 +21,8 @@ describe('Issuable', () => {
});
describe('resetIncomingEmailToken', () => {
+ let mock;
+
beforeEach(() => {
const element = document.createElement('a');
element.classList.add('incoming-email-token-reset');
@@ -30,14 +34,28 @@ describe('Issuable', () => {
document.body.appendChild(input);
Issuable = new IssuableIndex('issue_');
+
+ mock = new MockAdaptor(axios);
+
+ mock.onPut('foo').reply(200, {
+ new_address: 'testing123',
+ });
});
- it('should send request to reset email token', () => {
- spyOn(jQuery, 'ajax').and.callThrough();
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should send request to reset email token', (done) => {
+ spyOn(axios, 'put').and.callThrough();
document.querySelector('.incoming-email-token-reset').click();
- expect(jQuery.ajax).toHaveBeenCalled();
- expect(jQuery.ajax.calls.argsFor(0)[0].url).toEqual('foo');
+ setTimeout(() => {
+ expect(axios.put).toHaveBeenCalledWith('foo');
+ expect($('#issuable_email').val()).toBe('testing123');
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 2cd2e63b15d..177962ecf82 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,4 +1,6 @@
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Issue from '~/issue';
import '~/lib/utils/text_utility';
@@ -68,40 +70,27 @@ describe('Issue', function() {
expect($btn).toHaveText(isIssueInitiallyOpen ? 'Close issue' : 'Reopen issue');
}
- describe('task lists', function() {
- beforeEach(function() {
- loadFixtures('issues/issue-with-task-list.html.raw');
- this.issue = new Issue();
- });
-
- it('submits an ajax request on tasklist:changed', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PATCH');
- expect(req.url).toBe(gl.TEST_HOST + '/frontend-fixtures/issues-project/issues/1.json'); // eslint-disable-line prefer-template
- expect(req.data.issue.description).not.toBe(null);
- });
-
- $('.js-task-list-field').trigger('tasklist:changed');
- });
- });
-
[true, false].forEach((isIssueInitiallyOpen) => {
describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
const action = isIssueInitiallyOpen ? 'close' : 'reopen';
+ let mock;
- function ajaxSpy(req) {
- if (req.url === this.$triggeredButton.attr('href')) {
- expect(req.type).toBe('PUT');
- expectNewBranchButtonState(true, false);
- return this.issueStateDeferred;
- } else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
- expect(req.type).toBe('GET');
+ function mockCloseButtonResponseSuccess(url, response) {
+ mock.onPut(url).reply(() => {
expectNewBranchButtonState(true, false);
- return this.canCreateBranchDeferred;
- }
- expect(req.url).toBe('unexpected');
- return null;
+ return [200, response];
+ });
+ }
+
+ function mockCloseButtonResponseError(url) {
+ mock.onPut(url).networkError();
+ }
+
+ function mockCanCreateBranch(canCreateBranch) {
+ mock.onGet(/(.*)\/can_create_branch$/).reply(200, {
+ can_create_branch: canCreateBranch,
+ });
}
beforeEach(function() {
@@ -111,6 +100,11 @@ describe('Issue', function() {
loadFixtures('issues/closed-issue.html.raw');
}
+ mock = new MockAdapter(axios);
+
+ mock.onGet(/(.*)\/related_branches$/).reply(200, {});
+ mock.onGet(/(.*)\/referenced_merge_requests$/).reply(200, {});
+
findElements(isIssueInitiallyOpen);
this.issue = new Issue();
expectIssueState(isIssueInitiallyOpen);
@@ -120,71 +114,89 @@ describe('Issue', function() {
this.$projectIssuesCounter = $('.issue_counter').first();
this.$projectIssuesCounter.text('1,001');
- this.issueStateDeferred = new jQuery.Deferred();
- this.canCreateBranchDeferred = new jQuery.Deferred();
+ spyOn(axios, 'get').and.callThrough();
+ });
- spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this));
+ afterEach(() => {
+ mock.restore();
+ $('div.flash-alert').remove();
});
- it(`${action}s the issue`, function() {
- this.$triggeredButton.trigger('click');
- this.issueStateDeferred.resolve({
+ it(`${action}s the issue`, function(done) {
+ mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
id: 34
});
- this.canCreateBranchDeferred.resolve({
- can_create_branch: !isIssueInitiallyOpen
- });
+ mockCanCreateBranch(!isIssueInitiallyOpen);
- expectIssueState(!isIssueInitiallyOpen);
- expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
- expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
- expectNewBranchButtonState(false, !isIssueInitiallyOpen);
+ this.$triggeredButton.trigger('click');
+
+ setTimeout(() => {
+ expectIssueState(!isIssueInitiallyOpen);
+ expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
+ expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
+ expectNewBranchButtonState(false, !isIssueInitiallyOpen);
+
+ done();
+ });
});
- it(`fails to ${action} the issue if saved:false`, function() {
- this.$triggeredButton.trigger('click');
- this.issueStateDeferred.resolve({
+ it(`fails to ${action} the issue if saved:false`, function(done) {
+ mockCloseButtonResponseSuccess(this.$triggeredButton.attr('href'), {
saved: false
});
- this.canCreateBranchDeferred.resolve({
- can_create_branch: isIssueInitiallyOpen
- });
+ mockCanCreateBranch(isIssueInitiallyOpen);
- expectIssueState(isIssueInitiallyOpen);
- expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
- expectErrorMessage();
- expect(this.$projectIssuesCounter.text()).toBe('1,001');
- expectNewBranchButtonState(false, isIssueInitiallyOpen);
+ this.$triggeredButton.trigger('click');
+
+ setTimeout(() => {
+ expectIssueState(isIssueInitiallyOpen);
+ expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
+ expectErrorMessage();
+ expect(this.$projectIssuesCounter.text()).toBe('1,001');
+ expectNewBranchButtonState(false, isIssueInitiallyOpen);
+
+ done();
+ });
});
- it(`fails to ${action} the issue if HTTP error occurs`, function() {
+ it(`fails to ${action} the issue if HTTP error occurs`, function(done) {
+ mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
+ mockCanCreateBranch(isIssueInitiallyOpen);
+
this.$triggeredButton.trigger('click');
- this.issueStateDeferred.reject();
- this.canCreateBranchDeferred.resolve({
- can_create_branch: isIssueInitiallyOpen
- });
- expectIssueState(isIssueInitiallyOpen);
- expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
- expectErrorMessage();
- expect(this.$projectIssuesCounter.text()).toBe('1,001');
- expectNewBranchButtonState(false, isIssueInitiallyOpen);
+ setTimeout(() => {
+ expectIssueState(isIssueInitiallyOpen);
+ expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
+ expectErrorMessage();
+ expect(this.$projectIssuesCounter.text()).toBe('1,001');
+ expectNewBranchButtonState(false, isIssueInitiallyOpen);
+
+ done();
+ });
});
it('disables the new branch button if Ajax call fails', function() {
+ mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
+ mock.onGet(/(.*)\/can_create_branch$/).networkError();
+
this.$triggeredButton.trigger('click');
- this.issueStateDeferred.reject();
- this.canCreateBranchDeferred.reject();
expectNewBranchButtonState(false, false);
});
- it('does not trigger Ajax call if new branch button is missing', function() {
+ it('does not trigger Ajax call if new branch button is missing', function(done) {
+ mockCloseButtonResponseError(this.$triggeredButton.attr('href'));
Issue.$btnNewBranch = $();
this.canCreateBranchDeferred = null;
this.$triggeredButton.trigger('click');
- this.issueStateDeferred.reject();
+
+ setTimeout(() => {
+ expect(axios.get).not.toHaveBeenCalled();
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/job_spec.js b/spec/javascripts/job_spec.js
index feb341d22e6..03b58e9c1d0 100644
--- a/spec/javascripts/job_spec.js
+++ b/spec/javascripts/job_spec.js
@@ -1,3 +1,5 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import '~/lib/utils/datetime_utility';
@@ -6,11 +8,29 @@ import '~/breakpoints';
describe('Job', () => {
const JOB_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1`;
+ let mock;
+ let response;
+
+ function waitForPromise() {
+ return new Promise(resolve => requestAnimationFrame(resolve));
+ }
preloadFixtures('builds/build-with-artifacts.html.raw');
beforeEach(() => {
loadFixtures('builds/build-with-artifacts.html.raw');
+
+ spyOn(urlUtils, 'visitUrl');
+
+ mock = new MockAdapter(axios);
+
+ mock.onGet(new RegExp(`${JOB_URL}/trace.json?(.*)`)).reply(() => [200, response]);
+ });
+
+ afterEach(() => {
+ mock.restore();
+
+ response = {};
});
describe('class constructor', () => {
@@ -55,170 +75,159 @@ describe('Job', () => {
});
describe('running build', () => {
- it('updates the build trace on an interval', function () {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
- const deferred3 = $.Deferred();
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
- spyOn(urlUtils, 'visitUrl');
-
- deferred1.resolve({
+ it('updates the build trace on an interval', function (done) {
+ response = {
html: '<span>Update<span>',
status: 'running',
state: 'newstate',
append: true,
complete: false,
- });
-
- deferred2.resolve();
-
- deferred3.resolve({
- html: '<span>More</span>',
- status: 'running',
- state: 'finalstate',
- append: true,
- complete: true,
- });
+ };
this.job = new Job();
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
- expect(this.job.state).toBe('newstate');
-
- jasmine.clock().tick(4001);
-
- expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
- expect(this.job.state).toBe('finalstate');
+ waitForPromise()
+ .then(() => {
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+ expect(this.job.state).toBe('newstate');
+
+ response = {
+ html: '<span>More</span>',
+ status: 'running',
+ state: 'finalstate',
+ append: true,
+ complete: true,
+ };
+ })
+ .then(() => jasmine.clock().tick(4001))
+ .then(waitForPromise)
+ .then(() => {
+ expect($('#build-trace .js-build-output').text()).toMatch(/UpdateMore/);
+ expect(this.job.state).toBe('finalstate');
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('replaces the entire build trace', () => {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
- const deferred3 = $.Deferred();
-
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
-
- spyOn(urlUtils, 'visitUrl');
-
- deferred1.resolve({
+ it('replaces the entire build trace', (done) => {
+ response = {
html: '<span>Update<span>',
status: 'running',
append: false,
complete: false,
- });
-
- deferred2.resolve();
-
- deferred3.resolve({
- html: '<span>Different</span>',
- status: 'running',
- append: false,
- });
+ };
this.job = new Job();
- expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
-
- jasmine.clock().tick(4001);
-
- expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
- expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+ waitForPromise()
+ .then(() => {
+ expect($('#build-trace .js-build-output').text()).toMatch(/Update/);
+
+ response = {
+ html: '<span>Different</span>',
+ status: 'running',
+ append: false,
+ };
+ })
+ .then(() => jasmine.clock().tick(4001))
+ .then(waitForPromise)
+ .then(() => {
+ expect($('#build-trace .js-build-output').text()).not.toMatch(/Update/);
+ expect($('#build-trace .js-build-output').text()).toMatch(/Different/);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
describe('truncated information', () => {
describe('when size is less than total', () => {
- it('shows information about truncated log', () => {
- spyOn(urlUtils, 'visitUrl');
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
-
- deferred.resolve({
+ it('shows information about truncated log', (done) => {
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
- });
+ };
this.job = new Job();
- expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+ waitForPromise()
+ .then(() => {
+ expect(document.querySelector('.js-truncated-info').classList).not.toContain('hidden');
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('shows the size in KiB', () => {
+ it('shows the size in KiB', (done) => {
const size = 50;
- spyOn(urlUtils, 'visitUrl');
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size,
total: 100,
- });
+ };
this.job = new Job();
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(size)}`);
+ waitForPromise()
+ .then(() => {
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${numberToHumanSize(size)}`);
+ })
+ .then(done)
+ .catch(done.fail);
});
- it('shows incremented size', () => {
- const deferred1 = $.Deferred();
- const deferred2 = $.Deferred();
- const deferred3 = $.Deferred();
-
- spyOn($, 'ajax').and.returnValues(deferred1.promise(), deferred2.promise(), deferred3.promise());
-
- spyOn(urlUtils, 'visitUrl');
-
- deferred1.resolve({
+ it('shows incremented size', (done) => {
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
- });
-
- deferred2.resolve();
+ };
this.job = new Job();
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(50)}`);
-
- jasmine.clock().tick(4001);
-
- deferred3.resolve({
- html: '<span>Update</span>',
- status: 'success',
- append: true,
- size: 10,
- total: 100,
- });
-
- expect(
- document.querySelector('.js-truncated-info-size').textContent.trim(),
- ).toEqual(`${numberToHumanSize(60)}`);
+ waitForPromise()
+ .then(() => {
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${numberToHumanSize(50)}`);
+
+ response = {
+ html: '<span>Update</span>',
+ status: 'success',
+ append: true,
+ size: 10,
+ total: 100,
+ };
+ })
+ .then(() => jasmine.clock().tick(4001))
+ .then(waitForPromise)
+ .then(() => {
+ expect(
+ document.querySelector('.js-truncated-info-size').textContent.trim(),
+ ).toEqual(`${numberToHumanSize(60)}`);
+ })
+ .then(done)
+ .catch(done.fail);
});
it('renders the raw link', () => {
- const deferred = $.Deferred();
- spyOn(urlUtils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
- });
+ };
this.job = new Job();
@@ -229,50 +238,50 @@ describe('Job', () => {
});
describe('when size is equal than total', () => {
- it('does not show the trunctated information', () => {
- const deferred = $.Deferred();
- spyOn(urlUtils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
+ it('does not show the trunctated information', (done) => {
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
- });
+ };
this.job = new Job();
- expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+ waitForPromise()
+ .then(() => {
+ expect(document.querySelector('.js-truncated-info').classList).toContain('hidden');
+ })
+ .then(done)
+ .catch(done.fail);
});
});
});
describe('output trace', () => {
- beforeEach(() => {
- const deferred = $.Deferred();
- spyOn(urlUtils, 'visitUrl');
-
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- deferred.resolve({
+ beforeEach((done) => {
+ response = {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
- });
+ };
this.job = new Job();
+
+ waitForPromise()
+ .then(done)
+ .catch(done.fail);
});
it('should render trace controls', () => {
const controllers = document.querySelector('.controllers');
- expect(controllers.querySelector('.js-raw-link-controller')).toBeDefined();
- expect(controllers.querySelector('.js-erase-link')).toBeDefined();
- expect(controllers.querySelector('.js-scroll-up')).toBeDefined();
- expect(controllers.querySelector('.js-scroll-down')).toBeDefined();
+ expect(controllers.querySelector('.js-raw-link-controller')).not.toBeNull();
+ expect(controllers.querySelector('.js-scroll-up')).not.toBeNull();
+ expect(controllers.querySelector('.js-scroll-down')).not.toBeNull();
});
it('should render received output', () => {
@@ -285,13 +294,13 @@ describe('Job', () => {
describe('getBuildTrace', () => {
it('should request build trace with state parameter', (done) => {
- spyOn(jQuery, 'ajax').and.callThrough();
+ spyOn(axios, 'get').and.callThrough();
// eslint-disable-next-line no-new
new Job();
setTimeout(() => {
- expect(jQuery.ajax).toHaveBeenCalledWith(
- { url: `${JOB_URL}/trace.json`, data: { state: '' } },
+ expect(axios.get).toHaveBeenCalledWith(
+ `${JOB_URL}/trace.json`, { params: { state: '' } },
);
done();
}, 0);
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 43532275121..43589d54be4 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -37,7 +37,7 @@ export default {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ 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',
@@ -54,7 +54,7 @@ export default {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
active: false,
@@ -107,10 +107,10 @@ export default {
username: 'root',
id: 1,
state: 'active',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
- author_gravatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
+ 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',
},
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js b/spec/javascripts/labels_issue_sidebar_spec.js
index a197b35f6fb..7d992f62f64 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js
+++ b/spec/javascripts/labels_issue_sidebar_spec.js
@@ -1,4 +1,6 @@
/* eslint-disable no-new */
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import IssuableContext from '~/issuable_context';
import LabelsSelect from '~/labels_select';
@@ -10,35 +12,44 @@ import '~/users_select';
(() => {
let saveLabelCount = 0;
+ let mock;
+
describe('Issue dropdown sidebar', () => {
preloadFixtures('static/issue_sidebar_label.html.raw');
beforeEach(() => {
loadFixtures('static/issue_sidebar_label.html.raw');
+
+ mock = new MockAdapter(axios);
+
new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
new LabelsSelect();
- spyOn(jQuery, 'ajax').and.callFake((req) => {
- const d = $.Deferred();
- let LABELS_DATA = [];
+ mock.onGet('/root/test/labels.json').reply(() => {
+ const labels = Array(10).fill().map((_, i) => ({
+ id: i,
+ title: `test ${i}`,
+ color: '#5CB85C',
+ }));
- if (req.url === '/root/test/labels.json') {
- for (let i = 0; i < 10; i += 1) {
- LABELS_DATA.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
- }
- } else if (req.url === '/root/test/issues/2.json') {
- const tmp = [];
- for (let i = 0; i < saveLabelCount; i += 1) {
- tmp.push({ id: i, title: `test ${i}`, color: '#5CB85C' });
- }
- LABELS_DATA = { labels: tmp };
- }
+ return [200, labels];
+ });
+
+ mock.onPut('/root/test/issues/2.json').reply(() => {
+ const labels = Array(saveLabelCount).fill().map((_, i) => ({
+ id: i,
+ title: `test ${i}`,
+ color: '#5CB85C',
+ }));
- d.resolve(LABELS_DATA);
- return d.promise();
+ return [200, { labels }];
});
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('changes collapsed tooltip when changing labels when less than 5', (done) => {
saveLabelCount = 5;
$('.edit-link').get(0).click();
diff --git a/spec/javascripts/lib/utils/ajax_cache_spec.js b/spec/javascripts/lib/utils/ajax_cache_spec.js
index 49971bd91e2..7603400b55e 100644
--- a/spec/javascripts/lib/utils/ajax_cache_spec.js
+++ b/spec/javascripts/lib/utils/ajax_cache_spec.js
@@ -1,3 +1,5 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import AjaxCache from '~/lib/utils/ajax_cache';
describe('AjaxCache', () => {
@@ -87,66 +89,53 @@ describe('AjaxCache', () => {
});
describe('retrieve', () => {
- let ajaxSpy;
+ let mock;
beforeEach(() => {
- spyOn(jQuery, 'ajax').and.callFake(url => ajaxSpy(url));
+ mock = new MockAdapter(axios);
+
+ spyOn(axios, 'get').and.callThrough();
+ });
+
+ afterEach(() => {
+ mock.restore();
});
it('stores and returns data from Ajax call if cache is empty', (done) => {
- ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- const deferred = $.Deferred();
- deferred.resolve(dummyResponse);
- return deferred.promise();
- };
+ mock.onGet(dummyEndpoint).reply(200, dummyResponse);
AjaxCache.retrieve(dummyEndpoint)
.then((data) => {
- expect(data).toBe(dummyResponse);
- expect(AjaxCache.internalStorage[dummyEndpoint]).toBe(dummyResponse);
+ expect(data).toEqual(dummyResponse);
+ expect(AjaxCache.internalStorage[dummyEndpoint]).toEqual(dummyResponse);
})
.then(done)
.catch(fail);
});
- it('makes no Ajax call if request is pending', () => {
- const responseDeferred = $.Deferred();
-
- ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- // neither reject nor resolve to keep request pending
- return responseDeferred.promise();
- };
-
- const unexpectedResponse = data => fail(`Did not expect response: ${data}`);
+ it('makes no Ajax call if request is pending', (done) => {
+ mock.onGet(dummyEndpoint).reply(200, dummyResponse);
AjaxCache.retrieve(dummyEndpoint)
- .then(unexpectedResponse)
+ .then(done)
.catch(fail);
AjaxCache.retrieve(dummyEndpoint)
- .then(unexpectedResponse)
+ .then(done)
.catch(fail);
- expect($.ajax.calls.count()).toBe(1);
+ expect(axios.get.calls.count()).toBe(1);
});
it('returns undefined if Ajax call fails and cache is empty', (done) => {
- const dummyStatusText = 'exploded';
- const dummyErrorMessage = 'server exploded';
- ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- const deferred = $.Deferred();
- deferred.reject(null, dummyStatusText, dummyErrorMessage);
- return deferred.promise();
- };
+ const errorMessage = 'Network Error';
+ mock.onGet(dummyEndpoint).networkError();
AjaxCache.retrieve(dummyEndpoint)
.then(data => fail(`Received unexpected data: ${JSON.stringify(data)}`))
.catch((error) => {
- expect(error.message).toBe(`${dummyEndpoint}: ${dummyErrorMessage}`);
- expect(error.textStatus).toBe(dummyStatusText);
+ expect(error.message).toBe(`${dummyEndpoint}: ${errorMessage}`);
+ expect(error.textStatus).toBe(errorMessage);
done();
})
.catch(fail);
@@ -154,7 +143,9 @@ describe('AjaxCache', () => {
it('makes no Ajax call if matching data exists', (done) => {
AjaxCache.internalStorage[dummyEndpoint] = dummyResponse;
- ajaxSpy = () => fail(new Error('expected no Ajax call!'));
+ mock.onGet(dummyEndpoint).reply(() => {
+ fail(new Error('expected no Ajax call!'));
+ });
AjaxCache.retrieve(dummyEndpoint)
.then((data) => {
@@ -171,12 +162,7 @@ describe('AjaxCache', () => {
AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse;
- ajaxSpy = (url) => {
- expect(url).toBe(dummyEndpoint);
- const deferred = $.Deferred();
- deferred.resolve(dummyResponse);
- return deferred.promise();
- };
+ mock.onGet(dummyEndpoint).reply(200, dummyResponse);
// Call without forceRetrieve param
AjaxCache.retrieve(dummyEndpoint)
@@ -189,7 +175,7 @@ describe('AjaxCache', () => {
// Call with forceRetrieve param
AjaxCache.retrieve(dummyEndpoint, true)
.then((data) => {
- expect(data).toBe(dummyResponse);
+ expect(data).toEqual(dummyResponse);
})
.then(done)
.catch(fail);
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 0a9d815f469..80430011aed 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -1,6 +1,7 @@
/* eslint-disable promise/catch-or-return */
-
+import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
+import MockAdapter from 'axios-mock-adapter';
describe('common_utils', () => {
describe('parseUrl', () => {
@@ -413,48 +414,48 @@ describe('common_utils', () => {
describe('setCiStatusFavicon', () => {
const BUILD_URL = `${gl.TEST_HOST}/frontend-fixtures/builds-project/-/jobs/1/status.json`;
+ let mock;
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
document.body.appendChild(favicon);
+ mock = new MockAdapter(axios);
});
afterEach(() => {
+ mock.restore();
document.body.removeChild(document.getElementById('favicon'));
});
- it('should reset favicon in case of error', () => {
- const favicon = document.getElementById('favicon');
- spyOn($, 'ajax').and.callFake(function (options) {
- options.error();
- expect(favicon.getAttribute('href')).toEqual('null');
- });
+ it('should reset favicon in case of error', (done) => {
+ mock.onGet(BUILD_URL).networkError();
- commonUtils.setCiStatusFavicon(BUILD_URL);
+ commonUtils.setCiStatusFavicon(BUILD_URL)
+ .then(() => {
+ const favicon = document.getElementById('favicon');
+ expect(favicon.getAttribute('href')).toEqual('null');
+ done();
+ })
+ // Error is already caught in catch() block of setCiStatusFavicon,
+ // It won't throw another error for us to catch
+ .catch(done.fail);
});
- it('should set page favicon to CI status favicon based on provided status', () => {
+ it('should set page favicon to CI status favicon based on provided status', (done) => {
const FAVICON_PATH = '//icon_status_success';
- const favicon = document.getElementById('favicon');
- spyOn($, 'ajax').and.callFake(function (options) {
- options.success({ favicon: FAVICON_PATH });
- expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH);
+ mock.onGet(BUILD_URL).reply(200, {
+ favicon: FAVICON_PATH,
});
- commonUtils.setCiStatusFavicon(BUILD_URL);
- });
- });
-
- describe('ajaxPost', () => {
- it('should perform `$.ajax` call and do `POST` request', () => {
- const requestURL = '/some/random/api';
- const data = { keyname: 'value' };
- const ajaxSpy = spyOn($, 'ajax').and.callFake(() => {});
-
- commonUtils.ajaxPost(requestURL, data);
- expect(ajaxSpy.calls.allArgs()[0][0].type).toEqual('POST');
+ commonUtils.setCiStatusFavicon(BUILD_URL)
+ .then(() => {
+ const favicon = document.getElementById('favicon');
+ expect(favicon.getAttribute('href')).toEqual(FAVICON_PATH);
+ done();
+ })
+ .catch(done.fail);
});
});
diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/javascripts/lib/utils/users_cache_spec.js
index ec6ea35952b..50371c8c5f6 100644
--- a/spec/javascripts/lib/utils/users_cache_spec.js
+++ b/spec/javascripts/lib/utils/users_cache_spec.js
@@ -92,7 +92,9 @@ describe('UsersCache', () => {
apiSpy = (query, options) => {
expect(query).toBe('');
expect(options).toEqual({ username: dummyUsername });
- return Promise.resolve([dummyUser]);
+ return Promise.resolve({
+ data: [dummyUser],
+ });
};
UsersCache.retrieve(dummyUsername)
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index bae3219b043..bdfd16ac995 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,5 +1,6 @@
/* eslint-disable space-before-function-paren, no-return-assign */
-
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import MergeRequest from '~/merge_request';
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import IssuablesHelper from '~/helpers/issuables_helper';
@@ -7,11 +8,24 @@ import IssuablesHelper from '~/helpers/issuables_helper';
(function() {
describe('MergeRequest', function() {
describe('task lists', function() {
+ let mock;
+
preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
beforeEach(function() {
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
+
+ spyOn(axios, 'patch').and.callThrough();
+ mock = new MockAdapter(axios);
+
+ mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+
return this.merge = new MergeRequest();
});
+
+ afterEach(() => {
+ mock.restore();
+ });
+
it('modifies the Markdown field', function() {
spyOn(jQuery, 'ajax').and.stub();
const changeEvent = document.createEvent('HTMLEvents');
@@ -21,14 +35,14 @@ import IssuablesHelper from '~/helpers/issuables_helper';
});
it('submits an ajax request on tasklist:changed', (done) => {
- spyOn(jQuery, 'ajax').and.callFake((req) => {
- expect(req.type).toBe('PATCH');
- expect(req.url).toBe(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`);
- expect(req.data.merge_request.description).not.toBe(null);
+ $('.js-task-list-field').trigger('tasklist:changed');
+
+ setTimeout(() => {
+ expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
+ merge_request: { description: '- [ ] Task List Item' },
+ });
done();
});
-
- $('.js-task-list-field').trigger('tasklist:changed');
});
});
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index a6be474805b..fda24db98b4 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,5 +1,6 @@
/* eslint-disable no-var, comma-dangle, object-shorthand */
-
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
@@ -46,7 +47,7 @@ import 'vendor/jquery.scrollTo';
describe('activateTab', function () {
beforeEach(function () {
- spyOn($, 'ajax').and.callFake(function () {});
+ spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
this.subject = this.class.activateTab;
});
@@ -148,7 +149,7 @@ import 'vendor/jquery.scrollTo';
describe('setCurrentAction', function () {
beforeEach(function () {
- spyOn($, 'ajax').and.callFake(function () {});
+ spyOn(axios, 'get').and.returnValue(Promise.resolve({ data: {} }));
this.subject = this.class.setCurrentAction;
});
@@ -214,13 +215,21 @@ import 'vendor/jquery.scrollTo';
});
describe('tabShown', () => {
+ let mock;
+
beforeEach(function () {
- spyOn($, 'ajax').and.callFake(function (options) {
- options.success({ html: '' });
+ mock = new MockAdapter(axios);
+ mock.onGet(/(.*)\/diffs\.json/).reply(200, {
+ data: { html: '' },
});
+
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
});
+ afterEach(() => {
+ mock.restore();
+ });
+
describe('with "Side-by-side"/parallel diff view', () => {
beforeEach(function () {
this.class.diffViewType = () => 'parallel';
@@ -292,16 +301,20 @@ import 'vendor/jquery.scrollTo';
it('triggers Ajax request to JSON endpoint', function (done) {
const url = '/foo/bar/merge_requests/1/diffs';
- spyOn(this.class, 'ajaxGet').and.callFake((options) => {
- expect(options.url).toEqual(`${url}.json`);
+
+ spyOn(axios, 'get').and.callFake((reqUrl) => {
+ expect(reqUrl).toBe(`${url}.json`);
+
done();
+
+ return Promise.resolve({ data: {} });
});
this.class.loadDiff(url);
});
it('triggers scroll event when diff already loaded', function (done) {
- spyOn(this.class, 'ajaxGet').and.callFake(() => done.fail());
+ spyOn(axios, 'get').and.callFake(done.fail);
spyOn(document, 'dispatchEvent');
this.class.diffsLoaded = true;
@@ -316,6 +329,7 @@ import 'vendor/jquery.scrollTo';
describe('with inline diff', () => {
let noteId;
let noteLineNumId;
+ let mock;
beforeEach(() => {
const diffsResponse = getJSONFixture(inlineChangesTabJsonFixture);
@@ -330,29 +344,40 @@ import 'vendor/jquery.scrollTo';
.attr('href')
.replace('#', '');
- spyOn($, 'ajax').and.callFake(function (options) {
- options.success(diffsResponse);
- });
+ mock = new MockAdapter(axios);
+ mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
+ });
+
+ afterEach(() => {
+ mock.restore();
});
describe('with note fragment hash', () => {
- it('should expand and scroll to linked fragment hash #note_xxx', function () {
+ it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(noteId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
- target: jasmine.any(Object),
- lineType: 'old',
- forceShow: true,
+ setTimeout(() => {
+ expect(noteId.length).toBeGreaterThan(0);
+ expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
+ target: jasmine.any(Object),
+ lineType: 'old',
+ forceShow: true,
+ });
+
+ done();
});
});
- it('should gracefully ignore non-existant fragment hash', function () {
+ it('should gracefully ignore non-existant fragment hash', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
+ setTimeout(() => {
+ expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
+
+ done();
+ });
});
});
@@ -370,6 +395,7 @@ import 'vendor/jquery.scrollTo';
describe('with parallel diff', () => {
let noteId;
let noteLineNumId;
+ let mock;
beforeEach(() => {
const diffsResponse = getJSONFixture(parallelChangesTabJsonFixture);
@@ -384,30 +410,40 @@ import 'vendor/jquery.scrollTo';
.attr('href')
.replace('#', '');
- spyOn($, 'ajax').and.callFake(function (options) {
- options.success(diffsResponse);
- });
+ mock = new MockAdapter(axios);
+ mock.onGet(/(.*)\/diffs\.json/).reply(200, diffsResponse);
+ });
+
+ afterEach(() => {
+ mock.restore();
});
describe('with note fragment hash', () => {
- it('should expand and scroll to linked fragment hash #note_xxx', function () {
+ it('should expand and scroll to linked fragment hash #note_xxx', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue(noteId);
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(noteId.length).toBeGreaterThan(0);
- expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
- target: jasmine.any(Object),
- lineType: 'new',
- forceShow: true,
+ setTimeout(() => {
+ expect(noteId.length).toBeGreaterThan(0);
+ expect(Notes.instance.toggleDiffNote).toHaveBeenCalledWith({
+ target: jasmine.any(Object),
+ lineType: 'new',
+ forceShow: true,
+ });
+
+ done();
});
});
- it('should gracefully ignore non-existant fragment hash', function () {
+ it('should gracefully ignore non-existant fragment hash', function (done) {
spyOn(urlUtils, 'getLocationHash').and.returnValue('note_something-that-does-not-exist');
this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
- expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
+ setTimeout(() => {
+ expect(Notes.instance.toggleDiffNote).not.toHaveBeenCalled();
+ done();
+ });
});
});
diff --git a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
index 481b46c3ac6..6fa6f44f953 100644
--- a/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
+++ b/spec/javascripts/mini_pipeline_graph_dropdown_spec.js
@@ -1,7 +1,9 @@
/* eslint-disable no-new */
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
-import '~/flash';
+import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Mini Pipeline Graph Dropdown', () => {
preloadFixtures('static/mini_dropdown_graph.html.raw');
@@ -27,6 +29,16 @@ describe('Mini Pipeline Graph Dropdown', () => {
});
describe('When dropdown is clicked', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
it('should call getBuildsList', () => {
const getBuildsListSpy = spyOn(
MiniPipelineGraph.prototype,
@@ -41,46 +53,55 @@ describe('Mini Pipeline Graph Dropdown', () => {
});
it('should make a request to the endpoint provided in the html', () => {
- const ajaxSpy = spyOn($, 'ajax').and.callFake(function () {});
+ const ajaxSpy = spyOn(axios, 'get').and.callThrough();
+
+ mock.onGet('foobar').reply(200, {
+ html: '',
+ });
new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
- expect(ajaxSpy.calls.allArgs()[0][0].url).toEqual('foobar');
+ expect(ajaxSpy.calls.allArgs()[0][0]).toEqual('foobar');
});
- it('should not close when user uses cmd/ctrl + click', () => {
- spyOn($, 'ajax').and.callFake(function (params) {
- params.success({
- html: `<li>
- <a class="mini-pipeline-graph-dropdown-item" href="#">
- <span class="ci-status-icon ci-status-icon-failed"></span>
- <span class="ci-build-text">build</span>
- </a>
- <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
- </li>`,
- });
+ it('should not close when user uses cmd/ctrl + click', (done) => {
+ mock.onGet('foobar').reply(200, {
+ html: `<li>
+ <a class="mini-pipeline-graph-dropdown-item" href="#">
+ <span class="ci-status-icon ci-status-icon-failed"></span>
+ <span class="ci-build-text">build</span>
+ </a>
+ <a class="ci-action-icon-wrapper js-ci-action-icon" href="#"></a>
+ </li>`,
});
new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
document.querySelector('.js-builds-dropdown-button').click();
- document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
-
- expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
+ timeoutPromise()
+ .then(() => {
+ document.querySelector('a.mini-pipeline-graph-dropdown-item').click();
+ })
+ .then(timeoutPromise)
+ .then(() => {
+ expect($('.js-builds-dropdown-list').is(':visible')).toEqual(true);
+ })
+ .then(done)
+ .catch(done.fail);
});
- });
- it('should close the dropdown when request returns an error', (done) => {
- spyOn($, 'ajax').and.callFake(options => options.error());
+ it('should close the dropdown when request returns an error', (done) => {
+ mock.onGet('foobar').networkError();
- new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
+ new MiniPipelineGraph({ container: '.js-builds-dropdown-tests' }).bindEvents();
- document.querySelector('.js-builds-dropdown-button').click();
+ document.querySelector('.js-builds-dropdown-button').click();
- setTimeout(() => {
- expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false);
- done();
- }, 0);
+ setTimeout(() => {
+ expect($('.js-builds-dropdown-tests .dropdown').hasClass('open')).toEqual(false);
+ done();
+ });
+ });
});
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index b020a1020df..f0c800c759d 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -107,7 +107,7 @@ export const note = {
"name": "Administrator",
"username": "root",
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"path": "/root"
},
"created_at": "2017-08-10T15:24:03.087Z",
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index a40821a5693..274d7591c71 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,11 +1,14 @@
/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, max-len */
import _ from 'underscore';
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
import 'autosize';
import '~/gl_form';
import '~/lib/utils/text_utility';
import '~/render_gfm';
import Notes from '~/notes';
+import timeoutPromise from './helpers/set_timeout_promise_helper';
(function() {
window.gon || (window.gon = {});
@@ -47,13 +50,24 @@ import Notes from '~/notes';
});
describe('task lists', function() {
+ let mock;
+
beforeEach(function() {
+ spyOn(axios, 'patch').and.callThrough();
+ mock = new MockAdapter(axios);
+
+ mock.onPatch(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`).reply(200, {});
+
$('.js-comment-button').on('click', function(e) {
e.preventDefault();
});
this.notes = new Notes('', []);
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('modifies the Markdown field', function() {
const changeEvent = document.createEvent('HTMLEvents');
changeEvent.initEvent('change', true, true);
@@ -62,14 +76,15 @@ import Notes from '~/notes';
expect($('.js-task-list-field.original-task-list').val()).toBe('- [x] Task List Item');
});
- it('submits an ajax request on tasklist:changed', function() {
- spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PATCH');
- expect(req.url).toBe('http://test.host/frontend-fixtures/merge-requests-project/merge_requests/1.json');
- return expect(req.data.note).not.toBe(null);
- });
+ it('submits an ajax request on tasklist:changed', function(done) {
+ $('.js-task-list-container').trigger('tasklist:changed');
- $('.js-task-list-field.js-note-text').trigger('tasklist:changed');
+ setTimeout(() => {
+ expect(axios.patch).toHaveBeenCalledWith(`${gl.TEST_HOST}/frontend-fixtures/merge-requests-project/merge_requests/1.json`, {
+ note: { note: '' },
+ });
+ done();
+ });
});
});
@@ -119,6 +134,7 @@ import Notes from '~/notes';
let noteEntity;
let $form;
let $notesContainer;
+ let mock;
beforeEach(() => {
this.notes = new Notes('', []);
@@ -136,24 +152,32 @@ import Notes from '~/notes';
$form = $('form.js-main-target-form');
$notesContainer = $('ul.main-notes-list');
$form.find('textarea.js-note-text').val(sampleComment);
+
+ mock = new MockAdapter(axios);
+ mock.onPost(/(.*)\/notes$/).reply(200, noteEntity);
});
- it('updates note and resets edit form', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('updates note and resets edit form', (done) => {
spyOn(this.notes, 'revertNoteEditForm');
spyOn(this.notes, 'setupNewNote');
$('.js-comment-button').click();
- deferred.resolve(noteEntity);
- const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
- const updatedNote = Object.assign({}, noteEntity);
- updatedNote.note = 'bar';
- this.notes.updateNote(updatedNote, $targetNote);
+ setTimeout(() => {
+ const $targetNote = $notesContainer.find(`#note_${noteEntity.id}`);
+ const updatedNote = Object.assign({}, noteEntity);
+ updatedNote.note = 'bar';
+ this.notes.updateNote(updatedNote, $targetNote);
+
+ expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
+ expect(this.notes.setupNewNote).toHaveBeenCalled();
- expect(this.notes.revertNoteEditForm).toHaveBeenCalledWith($targetNote);
- expect(this.notes.setupNewNote).toHaveBeenCalled();
+ done();
+ });
});
});
@@ -479,8 +503,19 @@ import Notes from '~/notes';
};
let $form;
let $notesContainer;
+ let mock;
+
+ function mockNotesPost() {
+ mock.onPost(/(.*)\/notes$/).reply(200, note);
+ }
+
+ function mockNotesPostError() {
+ mock.onPost(/(.*)\/notes$/).networkError();
+ }
beforeEach(() => {
+ mock = new MockAdapter(axios);
+
this.notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
@@ -489,63 +524,92 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').val(sampleComment);
});
+ afterEach(() => {
+ mock.restore();
+ });
+
it('should show placeholder note while new comment is being posted', () => {
+ mockNotesPost();
+
$('.js-comment-button').click();
expect($notesContainer.find('.note.being-posted').length > 0).toEqual(true);
});
- it('should remove placeholder note when new comment is done posting', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should remove placeholder note when new comment is done posting', (done) => {
+ mockNotesPost();
+
$('.js-comment-button').click();
- deferred.resolve(note);
- expect($notesContainer.find('.note.being-posted').length).toEqual(0);
+ setTimeout(() => {
+ expect($notesContainer.find('.note.being-posted').length).toEqual(0);
+
+ done();
+ });
});
- it('should show actual note element when new comment is done posting', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should show actual note element when new comment is done posting', (done) => {
+ mockNotesPost();
+
$('.js-comment-button').click();
- deferred.resolve(note);
- expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
+ setTimeout(() => {
+ expect($notesContainer.find(`#note_${note.id}`).length > 0).toEqual(true);
+
+ done();
+ });
});
- it('should reset Form when new comment is done posting', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should reset Form when new comment is done posting', (done) => {
+ mockNotesPost();
+
$('.js-comment-button').click();
- deferred.resolve(note);
- expect($form.find('textarea.js-note-text').val()).toEqual('');
+ setTimeout(() => {
+ expect($form.find('textarea.js-note-text').val()).toEqual('');
+
+ done();
+ });
});
- it('should show flash error message when new comment failed to be posted', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should show flash error message when new comment failed to be posted', (done) => {
+ mockNotesPostError();
+
$('.js-comment-button').click();
- deferred.reject();
- expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
+ setTimeout(() => {
+ expect($notesContainer.parent().find('.flash-container .flash-text').is(':visible')).toEqual(true);
+
+ done();
+ });
});
- it('should show flash error message when comment failed to be updated', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ it('should show flash error message when comment failed to be updated', (done) => {
+ mockNotesPost();
+
$('.js-comment-button').click();
- deferred.resolve(note);
- const $noteEl = $notesContainer.find(`#note_${note.id}`);
- $noteEl.find('.js-note-edit').click();
- $noteEl.find('textarea.js-note-text').val(updatedComment);
- $noteEl.find('.js-comment-save-button').click();
+ timeoutPromise()
+ .then(() => {
+ const $noteEl = $notesContainer.find(`#note_${note.id}`);
+ $noteEl.find('.js-note-edit').click();
+ $noteEl.find('textarea.js-note-text').val(updatedComment);
- deferred.reject();
- const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
- expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
- expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
+ mock.restore();
+
+ mockNotesPostError();
+
+ $noteEl.find('.js-comment-save-button').click();
+ })
+ .then(timeoutPromise)
+ .then(() => {
+ const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`);
+ expect($updatedNoteEl.hasClass('.being-posted')).toEqual(false); // Remove being-posted visuals
+ expect($updatedNoteEl.find('.note-text').text().trim()).toEqual(sampleComment); // See if comment reverted back to original
+ expect($('.flash-container').is(':visible')).toEqual(true); // Flash error message shown
+
+ done();
+ })
+ .catch(done.fail);
});
});
@@ -563,8 +627,12 @@ import Notes from '~/notes';
};
let $form;
let $notesContainer;
+ let mock;
beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onPost(/(.*)\/notes$/).reply(200, note);
+
this.notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
@@ -582,15 +650,20 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').val(sampleComment);
});
- it('should remove slash command placeholder when comment with slash commands is done posting', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should remove slash command placeholder when comment with slash commands is done posting', (done) => {
spyOn(gl.awardsHandler, 'addAwardToEmojiBar').and.callThrough();
$('.js-comment-button').click();
expect($notesContainer.find('.system-note.being-posted').length).toEqual(1); // Placeholder shown
- deferred.resolve(note);
- expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
+
+ setTimeout(() => {
+ expect($notesContainer.find('.system-note.being-posted').length).toEqual(0); // Placeholder removed
+ done();
+ });
});
});
@@ -607,8 +680,12 @@ import Notes from '~/notes';
};
let $form;
let $notesContainer;
+ let mock;
beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onPost(/(.*)\/notes$/).reply(200, note);
+
this.notes = new Notes('', []);
window.gon.current_username = 'root';
window.gon.current_user_fullname = 'Administrator';
@@ -617,19 +694,24 @@ import Notes from '~/notes';
$form.find('textarea.js-note-text').html(sampleComment);
});
- it('should not render a script tag', () => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should not render a script tag', (done) => {
$('.js-comment-button').click();
- deferred.resolve(note);
- const $noteEl = $notesContainer.find(`#note_${note.id}`);
- $noteEl.find('.js-note-edit').click();
- $noteEl.find('textarea.js-note-text').html(updatedComment);
- $noteEl.find('.js-comment-save-button').click();
+ setTimeout(() => {
+ const $noteEl = $notesContainer.find(`#note_${note.id}`);
+ $noteEl.find('.js-note-edit').click();
+ $noteEl.find('textarea.js-note-text').html(updatedComment);
+ $noteEl.find('.js-comment-save-button').click();
+
+ const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container');
+ expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
- const $updatedNoteEl = $notesContainer.find(`#note_${note.id}`).find('.js-task-list-container');
- expect($updatedNoteEl.find('.note-text').text().trim()).toEqual('');
+ done();
+ });
});
});
diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js
index 2fd87754238..b09494f0b77 100644
--- a/spec/javascripts/pager_spec.js
+++ b/spec/javascripts/pager_spec.js
@@ -1,5 +1,6 @@
/* global fixture */
-
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import * as utils from '~/lib/utils/url_utility';
import Pager from '~/pager';
@@ -9,7 +10,6 @@ describe('pager', () => {
beforeEach(() => {
setFixtures('<div class="content_list"></div><div class="loading"></div>');
- spyOn($, 'ajax');
});
afterEach(() => {
@@ -47,39 +47,90 @@ describe('pager', () => {
});
describe('getOld', () => {
+ const urlRegex = /(.*)some_list(.*)$/;
+ let mock;
+
+ function mockSuccess() {
+ mock.onGet(urlRegex).reply(200, {
+ count: 0,
+ html: '',
+ });
+ }
+
+ function mockError() {
+ mock.onGet(urlRegex).networkError();
+ }
+
beforeEach(() => {
setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>');
+ spyOn(axios, 'get').and.callThrough();
+
+ mock = new MockAdapter(axios);
+
Pager.init();
});
- it('shows loader while loading next page', () => {
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('shows loader while loading next page', (done) => {
+ mockSuccess();
+
spyOn(Pager.loading, 'show');
Pager.getOld();
- expect(Pager.loading.show).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(Pager.loading.show).toHaveBeenCalled();
+
+ done();
+ });
});
- it('hides loader on success', () => {
- spyOn($, 'ajax').and.callFake(options => options.success({}));
+ it('hides loader on success', (done) => {
+ mockSuccess();
+
spyOn(Pager.loading, 'hide');
Pager.getOld();
- expect(Pager.loading.hide).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(Pager.loading.hide).toHaveBeenCalled();
+
+ done();
+ });
});
- it('hides loader on error', () => {
- spyOn($, 'ajax').and.callFake(options => options.error());
+ it('hides loader on error', (done) => {
+ mockError();
+
spyOn(Pager.loading, 'hide');
Pager.getOld();
- expect(Pager.loading.hide).toHaveBeenCalled();
+
+ setTimeout(() => {
+ expect(Pager.loading.hide).toHaveBeenCalled();
+
+ done();
+ });
});
- it('sends request to url with offset and limit params', () => {
- spyOn($, 'ajax');
+ it('sends request to url with offset and limit params', (done) => {
Pager.offset = 100;
Pager.limit = 20;
Pager.getOld();
- const [{ data, url }] = $.ajax.calls.argsFor(0);
- expect(data).toBe('limit=20&offset=100');
- expect(url).toBe('/some_list');
+
+ setTimeout(() => {
+ const [url, params] = axios.get.calls.argsFor(0);
+
+ expect(params).toEqual({
+ params: {
+ limit: 20,
+ offset: 100,
+ },
+ });
+ expect(url).toBe('/some_list');
+
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
new file mode 100644
index 00000000000..3cd33a3e900
--- /dev/null
+++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+
+import axios from '~/lib/utils/axios_utils';
+import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
+import eventHub from '~/pages/milestones/shared/event_hub';
+import * as urlUtility from '~/lib/utils/url_utility';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+describe('delete_milestone_modal.vue', () => {
+ const Component = Vue.extend(deleteMilestoneModal);
+ const props = {
+ issueCount: 1,
+ mergeRequestCount: 2,
+ milestoneId: 3,
+ milestoneTitle: 'my milestone title',
+ milestoneUrl: `${gl.TEST_HOST}/delete_milestone_modal.vue/milestone`,
+ };
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('onSubmit', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ spyOn(eventHub, '$emit');
+ });
+
+ it('deletes milestone and redirects to overview page', (done) => {
+ const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`;
+ spyOn(axios, 'delete').and.callFake((url) => {
+ expect(url).toBe(props.milestoneUrl);
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl);
+ eventHub.$emit.calls.reset();
+ return Promise.resolve({
+ request: {
+ responseURL,
+ },
+ });
+ });
+ const redirectSpy = spyOn(urlUtility, 'redirectTo');
+
+ vm.onSubmit()
+ .then(() => {
+ expect(redirectSpy).toHaveBeenCalledWith(responseURL);
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: true });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('displays error if deleting milestone failed', (done) => {
+ const dummyError = new Error('deleting milestone failed');
+ dummyError.response = { status: 418 };
+ spyOn(axios, 'delete').and.callFake((url) => {
+ expect(url).toBe(props.milestoneUrl);
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestStarted', props.milestoneUrl);
+ eventHub.$emit.calls.reset();
+ return Promise.reject(dummyError);
+ });
+ const redirectSpy = spyOn(urlUtility, 'redirectTo');
+
+ vm.onSubmit()
+ .catch((error) => {
+ expect(error).toBe(dummyError);
+ expect(redirectSpy).not.toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', { milestoneUrl: props.milestoneUrl, successful: false });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('text', () => {
+ it('contains the issue and milestone count', () => {
+ vm = mountComponent(Component, props);
+ const value = vm.text;
+
+ expect(value).toContain('remove it from 1 issue and 2 merge requests');
+ });
+
+ it('contains neither issue nor milestone count', () => {
+ vm = mountComponent(Component, { ...props,
+ issueCount: 0,
+ mergeRequestCount: 0,
+ });
+
+ const value = vm.text;
+
+ expect(value).toContain('is not currently used');
+ });
+ });
+});
diff --git a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js b/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js
deleted file mode 100644
index 5b316b319a5..00000000000
--- a/spec/javascripts/pipeline_schedules/setup_pipeline_variable_list_spec.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import {
- setupPipelineVariableList,
- insertRow,
- removeRow,
-} from '~/pipeline_schedules/setup_pipeline_variable_list';
-
-describe('Pipeline Variable List', () => {
- let $markup;
-
- describe('insertRow', () => {
- it('should insert another row', () => {
- $markup = $(`<div>
- <li class="js-row">
- <input>
- <textarea></textarea>
- </li>
- </div>`);
-
- insertRow($markup.find('.js-row'));
-
- expect($markup.find('.js-row').length).toBe(2);
- });
-
- it('should clear `data-is-persisted` on cloned row', () => {
- $markup = $(`<div>
- <li class="js-row" data-is-persisted="true"></li>
- </div>`);
-
- insertRow($markup.find('.js-row'));
-
- const $lastRow = $markup.find('.js-row').last();
- expect($lastRow.attr('data-is-persisted')).toBe(undefined);
- });
-
- it('should clear inputs on cloned row', () => {
- $markup = $(`<div>
- <li class="js-row">
- <input value="foo">
- <textarea>bar</textarea>
- </li>
- </div>`);
-
- insertRow($markup.find('.js-row'));
-
- const $lastRow = $markup.find('.js-row').last();
- expect($lastRow.find('input').val()).toBe('');
- expect($lastRow.find('textarea').val()).toBe('');
- });
- });
-
- describe('removeRow', () => {
- it('should remove dynamic row', () => {
- $markup = $(`<div>
- <li class="js-row">
- <input>
- <textarea></textarea>
- </li>
- </div>`);
-
- removeRow($markup.find('.js-row'));
-
- expect($markup.find('.js-row').length).toBe(0);
- });
-
- it('should hide and mark to destroy with already persisted rows', () => {
- $markup = $(`<div>
- <li class="js-row" data-is-persisted="true">
- <input class="js-destroy-input">
- </li>
- </div>`);
-
- const $row = $markup.find('.js-row');
- removeRow($row);
-
- expect($row.find('.js-destroy-input').val()).toBe('1');
- expect($markup.find('.js-row').length).toBe(1);
- });
- });
-
- describe('setupPipelineVariableList', () => {
- beforeEach(() => {
- $markup = $(`<form>
- <li class="js-row">
- <input class="js-user-input" name="schedule[variables_attributes][][key]">
- <textarea class="js-user-input" name="schedule[variables_attributes][][value]"></textarea>
- <button class="js-row-remove-button"></button>
- <button class="js-row-add-button"></button>
- </li>
- </form>`);
-
- setupPipelineVariableList($markup);
- });
-
- it('should remove the row when clicking the remove button', () => {
- $markup.find('.js-row-remove-button').trigger('click');
-
- expect($markup.find('.js-row').length).toBe(0);
- });
-
- it('should add another row when editing the last rows key input', () => {
- const $row = $markup.find('.js-row');
- $row.find('input.js-user-input')
- .val('foo')
- .trigger('input');
-
- expect($markup.find('.js-row').length).toBe(2);
- });
-
- it('should add another row when editing the last rows value textarea', () => {
- const $row = $markup.find('.js-row');
- $row.find('textarea.js-user-input')
- .val('foo')
- .trigger('input');
-
- expect($markup.find('.js-row').length).toBe(2);
- });
-
- it('should remove empty row after blurring', () => {
- const $row = $markup.find('.js-row');
- $row.find('input.js-user-input')
- .val('foo')
- .trigger('input');
-
- expect($markup.find('.js-row').length).toBe(2);
-
- $row.find('input.js-user-input')
- .val('')
- .trigger('input')
- .trigger('blur');
-
- expect($markup.find('.js-row').length).toBe(1);
- });
-
- it('should clear out the `name` attribute on the inputs for the last empty row on form submission (avoid BE validation)', () => {
- const $row = $markup.find('.js-row');
- expect($row.find('input').attr('name')).toBe('schedule[variables_attributes][][key]');
- expect($row.find('textarea').attr('name')).toBe('schedule[variables_attributes][][value]');
-
- $markup.filter('form').submit();
-
- expect($row.find('input').attr('name')).toBe('');
- expect($row.find('textarea').attr('name')).toBe('');
- });
- });
-});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index a9126d2f4e9..de744739e42 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -24,9 +24,10 @@ describe('Pipelines Table Row', () => {
beforeEach(() => {
const pipelines = getJSONFixture(jsonFixtureName).pipelines;
- pipeline = pipelines.find(p => p.id === 1);
- pipelineWithoutAuthor = pipelines.find(p => p.id === 2);
- pipelineWithoutCommit = pipelines.find(p => p.id === 3);
+
+ pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
+ pipelineWithoutAuthor = pipelines.find(p => p.user === null && p.commit !== null);
+ pipelineWithoutCommit = pipelines.find(p => p.user === null && p.commit === null);
});
afterEach(() => {
diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js
index ca2f9163313..4fc3c08145e 100644
--- a/spec/javascripts/pipelines/pipelines_table_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_spec.js
@@ -11,9 +11,10 @@ describe('Pipelines Table', () => {
preloadFixtures(jsonFixtureName);
beforeEach(() => {
- PipelinesTableComponent = Vue.extend(pipelinesTableComp);
const pipelines = getJSONFixture(jsonFixtureName).pipelines;
- pipeline = pipelines.find(p => p.id === 1);
+
+ PipelinesTableComponent = Vue.extend(pipelinesTableComp);
+ pipeline = pipelines.find(p => p.user !== null && p.commit !== null);
});
describe('table', () => {
diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
index b24567ffc0c..f6c0f51cf62 100644
--- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
@@ -1,3 +1,5 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import PANEL_STATE from '~/prometheus_metrics/constants';
import { metrics, missingVarMetrics } from './mock_data';
@@ -102,25 +104,38 @@ describe('PrometheusMetrics', () => {
describe('loadActiveMetrics', () => {
let prometheusMetrics;
+ let mock;
+
+ function mockSuccess() {
+ mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(200, {
+ data: metrics,
+ success: true,
+ });
+ }
+
+ function mockError() {
+ mock.onGet(prometheusMetrics.activeMetricsEndpoint).networkError();
+ }
beforeEach(() => {
+ spyOn(axios, 'get').and.callThrough();
+
prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
+
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
});
it('should show loader animation while response is being loaded and hide it when request is complete', (done) => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
+ mockSuccess();
prometheusMetrics.loadActiveMetrics();
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
- expect($.ajax).toHaveBeenCalledWith({
- url: prometheusMetrics.activeMetricsEndpoint,
- dataType: 'json',
- global: false,
- });
-
- deferred.resolve({ data: metrics, success: true });
+ expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
setTimeout(() => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
@@ -129,14 +144,10 @@ describe('PrometheusMetrics', () => {
});
it('should show empty state if response failed to load', (done) => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
- spyOn(prometheusMetrics, 'populateActiveMetrics');
+ mockError();
prometheusMetrics.loadActiveMetrics();
- deferred.reject();
-
setTimeout(() => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
@@ -145,14 +156,11 @@ describe('PrometheusMetrics', () => {
});
it('should populate metrics list once response is loaded', (done) => {
- const deferred = $.Deferred();
- spyOn($, 'ajax').and.returnValue(deferred.promise());
spyOn(prometheusMetrics, 'populateActiveMetrics');
+ mockSuccess();
prometheusMetrics.loadActiveMetrics();
- deferred.resolve({ data: metrics, success: true });
-
setTimeout(() => {
expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
done();
diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
index 233cca06ed0..8bbc3100357 100644
--- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
@@ -18,8 +18,10 @@ describe('new file modal component', () => {
}));
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
- commit: {
- id: '123branch',
+ data: {
+ commit: {
+ id: '123branch',
+ },
},
}));
diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
index 788c08e5279..667112ab21a 100644
--- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
@@ -17,8 +17,10 @@ describe('new dropdown upload', () => {
}));
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
- commit: {
- id: '123branch',
+ data: {
+ commit: {
+ id: '123branch',
+ },
},
}));
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index 676ac09f2c9..93e94b4f24c 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -87,8 +87,10 @@ describe('RepoCommitSection', () => {
changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles));
spyOn(service, 'commit').and.returnValue(Promise.resolve({
- short_id: '1',
- stats: {},
+ data: {
+ short_id: '1',
+ stats: {},
+ },
}));
});
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
index 8d830c67290..f678967b092 100644
--- a/spec/javascripts/repo/stores/actions_spec.js
+++ b/spec/javascripts/repo/stores/actions_spec.js
@@ -178,7 +178,9 @@ describe('Multi-file store actions', () => {
it('calls service', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
- commit: { id: '123' },
+ data: {
+ commit: { id: '123' },
+ },
}));
store.dispatch('checkCommitStatus')
@@ -192,7 +194,9 @@ describe('Multi-file store actions', () => {
it('returns true if current ref does not equal returned ID', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
- commit: { id: '123' },
+ data: {
+ commit: { id: '123' },
+ },
}));
store.dispatch('checkCommitStatus')
@@ -206,7 +210,9 @@ describe('Multi-file store actions', () => {
it('returns false if current ref equals returned ID', (done) => {
spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
- commit: { id: '1' },
+ data: {
+ commit: { id: '1' },
+ },
}));
store.dispatch('checkCommitStatus')
@@ -250,13 +256,15 @@ describe('Multi-file store actions', () => {
describe('success', () => {
beforeEach(() => {
spyOn(service, 'commit').and.returnValue(Promise.resolve({
- id: '123456',
- short_id: '123',
- message: 'test message',
- committed_date: 'date',
- stats: {
- additions: '1',
- deletions: '2',
+ data: {
+ id: '123456',
+ short_id: '123',
+ message: 'test message',
+ committed_date: 'date',
+ stats: {
+ additions: '1',
+ deletions: '2',
+ },
},
}));
});
@@ -324,7 +332,9 @@ describe('Multi-file store actions', () => {
describe('failed', () => {
beforeEach(() => {
spyOn(service, 'commit').and.returnValue(Promise.resolve({
- message: 'failed message',
+ data: {
+ message: 'failed message',
+ },
}));
});
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 3267e29585b..35bb630bf5d 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,6 +1,8 @@
/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, max-len */
+import MockAdapter from 'axios-mock-adapter';
import '~/commons/bootstrap';
+import axios from '~/lib/utils/axios_utils';
import Sidebar from '~/right_sidebar';
(function() {
@@ -35,16 +37,23 @@ import Sidebar from '~/right_sidebar';
var fixtureName = 'issues/open-issue.html.raw';
preloadFixtures(fixtureName);
loadJSONFixtures('todos/todos.json');
+ let mock;
beforeEach(function() {
loadFixtures(fixtureName);
- this.sidebar = new Sidebar;
+ mock = new MockAdapter(axios);
+ this.sidebar = new Sidebar();
$aside = $('.right-sidebar');
$page = $('.layout-page');
$icon = $aside.find('i');
$toggle = $aside.find('.js-sidebar-toggle');
return $labelsIcon = $aside.find('.sidebar-collapsed-icon');
});
+
+ afterEach(() => {
+ mock.restore();
+ });
+
it('should expand/collapse the sidebar when arrow is clicked', function() {
assertSidebarState('expanded');
$toggle.click();
@@ -63,20 +72,19 @@ import Sidebar from '~/right_sidebar';
return assertSidebarState('collapsed');
});
- it('should broadcast todo:toggle event when add todo clicked', function() {
+ it('should broadcast todo:toggle event when add todo clicked', function(done) {
var todos = getJSONFixture('todos/todos.json');
- spyOn(jQuery, 'ajax').and.callFake(function() {
- var d = $.Deferred();
- var response = todos;
- d.resolve(response);
- return d.promise();
- });
+ mock.onPost(/(.*)\/todos$/).reply(200, todos);
var todoToggleSpy = spyOnEvent(document, 'todo:toggle');
$('.issuable-sidebar-header .js-issuable-todo').click();
- expect(todoToggleSpy.calls.count()).toEqual(1);
+ setTimeout(() => {
+ expect(todoToggleSpy.calls.count()).toEqual(1);
+
+ done();
+ });
});
it('should not hide collapsed icons', () => {
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index 7bc591d2d47..d9e84e35f69 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -27,7 +27,7 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/user0',
},
{
@@ -35,7 +35,7 @@ const RESPONSE_MAP = {
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/tajuana',
},
{
@@ -43,7 +43,7 @@ const RESPONSE_MAP = {
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
web_url: 'http: //localhost:3001/michaele.will',
},
],
@@ -72,24 +72,24 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/user0',
+ avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/user0',
},
{
name: 'Marguerite Bartell',
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/tajuana',
+ avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/tajuana',
},
{
name: 'Laureen Ritchie',
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/michaele.will',
+ avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/michaele.will',
},
],
human_time_estimate: null,
@@ -100,24 +100,24 @@ const RESPONSE_MAP = {
username: 'user0',
id: 22,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/user0',
+ avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/user0',
},
{
name: 'Marguerite Bartell',
username: 'tajuana',
id: 18,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/tajuana',
+ avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/tajuana',
},
{
name: 'Laureen Ritchie',
username: 'michaele.will',
id: 16,
state: 'active',
- avatar_url: 'http: //www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
- web_url: 'http: //localhost:3001/michaele.will',
+ avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+ web_url: 'http://localhost:3001/michaele.will',
},
],
subscribed: true,
@@ -182,7 +182,7 @@ const mockData = {
id: 1,
name: 'Administrator',
username: 'root',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
rootPath: '/',
fullPath: '/gitlab-org/gitlab-shell',
@@ -194,7 +194,7 @@ const mockData = {
human_total_time_spent: null,
},
user: {
- avatar: 'http://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar: 'https://gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
id: 1,
name: 'Administrator',
username: 'root',
diff --git a/spec/javascripts/sidebar/sidebar_store_spec.js b/spec/javascripts/sidebar/sidebar_store_spec.js
index ea4eae1e23f..3591f96ff87 100644
--- a/spec/javascripts/sidebar/sidebar_store_spec.js
+++ b/spec/javascripts/sidebar/sidebar_store_spec.js
@@ -6,14 +6,14 @@ const ASSIGNEE = {
id: 2,
name: 'gitlab user 2',
username: 'gitlab2',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
};
const ANOTHER_ASSINEE = {
id: 3,
name: 'gitlab user 3',
username: 'gitlab3',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
};
const PARTICIPANT = {
@@ -38,7 +38,7 @@ describe('Sidebar store', () => {
id: 1,
name: 'Administrator',
username: 'root',
- avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
},
editable: true,
rootPath: '/',
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 2f6691df9cd..9b2a5379855 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -6,6 +6,8 @@ import '~/commons';
import Vue from 'vue';
import VueResource from 'vue-resource';
+import { getDefaultAdapter } from '~/lib/utils/axios_utils';
+
const isHeadlessChrome = /\bHeadlessChrome\//.test(navigator.userAgent);
Vue.config.devtools = !isHeadlessChrome;
Vue.config.productionTip = false;
@@ -59,6 +61,8 @@ beforeEach(() => {
Vue.http.interceptors = builtinVueHttpInterceptors.slice();
});
+const axiosDefaultAdapter = getDefaultAdapter();
+
// render all of our tests
const testsContext = require.context('.', true, /_spec$/);
testsContext.keys().forEach(function (path) {
@@ -94,6 +98,12 @@ describe('test errors', () => {
it('has no Vue error', () => {
expect(hasVueErrors).toBe(false);
});
+
+ it('restores axios adapter after mocking', () => {
+ if (getDefaultAdapter() !== axiosDefaultAdapter) {
+ fail('axios adapter is not restored! Did you forget a restore() on MockAdapter?');
+ }
+ });
});
// if we're generating coverage reports, make sure to include all files so
diff --git a/spec/javascripts/toggle_buttons_spec.js b/spec/javascripts/toggle_buttons_spec.js
new file mode 100644
index 00000000000..205e396d682
--- /dev/null
+++ b/spec/javascripts/toggle_buttons_spec.js
@@ -0,0 +1,120 @@
+import setupToggleButtons from '~/toggle_buttons';
+import getSetTimeoutPromise from './helpers/set_timeout_promise_helper';
+
+function generateMarkup(isChecked = true) {
+ return `
+ <button type="button" class="${isChecked ? 'is-checked' : ''} js-project-feature-toggle">
+ <input type="hidden" class="js-project-feature-toggle-input" value="${isChecked}" />
+ </button>
+ `;
+}
+
+function setupFixture(isChecked, clickCallback) {
+ const wrapper = document.createElement('div');
+ wrapper.innerHTML = generateMarkup(isChecked);
+
+ setupToggleButtons(wrapper, clickCallback);
+
+ return wrapper;
+}
+
+describe('ToggleButtons', () => {
+ describe('when input value is true', () => {
+ it('should initialize as checked', () => {
+ const wrapper = setupFixture(true);
+
+ expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(true);
+ expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true');
+ });
+
+ it('should toggle to unchecked when clicked', (done) => {
+ const wrapper = setupFixture(true);
+ const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
+
+ toggleButton.click();
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(toggleButton.classList.contains('is-checked')).toEqual(false);
+ expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('when input value is false', () => {
+ it('should initialize as unchecked', () => {
+ const wrapper = setupFixture(false);
+
+ expect(wrapper.querySelector('.js-project-feature-toggle').classList.contains('is-checked')).toEqual(false);
+ expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('false');
+ });
+
+ it('should toggle to checked when clicked', (done) => {
+ const wrapper = setupFixture(false);
+ const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
+
+ toggleButton.click();
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(toggleButton.classList.contains('is-checked')).toEqual(true);
+ expect(wrapper.querySelector('.js-project-feature-toggle-input').value).toEqual('true');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ it('should emit `trigger-change` event', (done) => {
+ const changeSpy = jasmine.createSpy('changeEventHandler');
+ const wrapper = setupFixture(false);
+ const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
+ const input = wrapper.querySelector('.js-project-feature-toggle-input');
+
+ $(input).on('trigger-change', changeSpy);
+
+ toggleButton.click();
+
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(changeSpy).toHaveBeenCalled();
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ describe('clickCallback', () => {
+ it('should show loading indicator while waiting', (done) => {
+ const isChecked = true;
+ const clickCallback = (newValue, toggleButton) => {
+ const input = toggleButton.querySelector('.js-project-feature-toggle-input');
+
+ expect(newValue).toEqual(false);
+
+ // Check for the loading state
+ expect(toggleButton.classList.contains('is-checked')).toEqual(false);
+ expect(toggleButton.classList.contains('is-loading')).toEqual(true);
+ expect(toggleButton.disabled).toEqual(true);
+ expect(input.value).toEqual('true');
+
+ // After the callback finishes, check that the loading state is gone
+ getSetTimeoutPromise()
+ .then(() => {
+ expect(toggleButton.classList.contains('is-checked')).toEqual(false);
+ expect(toggleButton.classList.contains('is-loading')).toEqual(false);
+ expect(toggleButton.disabled).toEqual(false);
+ expect(input.value).toEqual('false');
+ })
+ .then(done)
+ .catch(done.fail);
+ };
+
+ const wrapper = setupFixture(isChecked, clickCallback);
+ const toggleButton = wrapper.querySelector('.js-project-feature-toggle');
+
+ toggleButton.click();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
index a750bc78f36..f14d5f6f76c 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
@@ -1,39 +1,39 @@
import Vue from 'vue';
-import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author';
-
-const author = {
- webUrl: 'http://foo.bar',
- avatarUrl: 'http://gravatar.com/foo',
- name: 'fatihacet',
-};
-const createComponent = () => {
- const Component = Vue.extend(authorComponent);
-
- return new Component({
- el: document.createElement('div'),
- propsData: { author },
- });
-};
+import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetAuthor', () => {
- describe('props', () => {
- it('should have props', () => {
- const authorProp = authorComponent.props.author;
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(authorComponent);
+
+ vm = mountComponent(Component, {
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
- expect(authorProp).toBeDefined();
- expect(authorProp.type instanceof Object).toBeTruthy();
- expect(authorProp.required).toBeTruthy();
});
});
- describe('template', () => {
- it('should have correct elements', () => {
- const el = createComponent().$el;
+ afterEach(() => {
+ vm.$destroy();
+ });
- expect(el.tagName).toEqual('A');
- expect(el.getAttribute('href')).toEqual(author.webUrl);
- expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl);
- expect(el.querySelector('.author').innerText.trim()).toEqual(author.name);
- });
+ it('renders link with the author web url', () => {
+ expect(vm.$el.getAttribute('href')).toEqual('http://localhost:3000/root');
+ });
+
+ it('renders image with avatar url', () => {
+ expect(
+ vm.$el.querySelector('img').getAttribute('src'),
+ ).toEqual('http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon');
+ });
+
+ it('renders author name', () => {
+ expect(vm.$el.textContent.trim()).toEqual('Administrator');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
index 515ddcbb875..8c55622b15e 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
@@ -1,61 +1,40 @@
import Vue from 'vue';
-import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time';
-
-const props = {
- actionText: 'Merged by',
- author: {
- webUrl: 'http://foo.bar',
- avatarUrl: 'http://gravatar.com/foo',
- name: 'fatihacet',
- },
- dateTitle: '2017-03-23T23:02:00.807Z',
- dateReadable: '12 hours ago',
-};
-const createComponent = () => {
- const Component = Vue.extend(authorTimeComponent);
-
- return new Component({
- el: document.createElement('div'),
- propsData: props,
- });
-};
+import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetAuthorTime', () => {
- describe('props', () => {
- it('should have props', () => {
- const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props;
- const ActionTextClass = actionText.type;
- const DateTitleClass = dateTitle.type;
- const DateReadableClass = dateReadable.type;
-
- expect(new ActionTextClass() instanceof String).toBeTruthy();
- expect(actionText.required).toBeTruthy();
-
- expect(author.type instanceof Object).toBeTruthy();
- expect(author.required).toBeTruthy();
-
- expect(new DateTitleClass() instanceof String).toBeTruthy();
- expect(dateTitle.required).toBeTruthy();
-
- expect(new DateReadableClass() instanceof String).toBeTruthy();
- expect(dateReadable.required).toBeTruthy();
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(authorTimeComponent);
+
+ vm = mountComponent(Component, {
+ actionText: 'Merged by',
+ author: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ dateTitle: '2017-03-23T23:02:00.807Z',
+ dateReadable: '12 hours ago',
});
});
- describe('components', () => {
- it('should have components', () => {
- expect(authorTimeComponent.components['mr-widget-author']).toBeDefined();
- });
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders provided action text', () => {
+ expect(vm.$el.textContent).toContain('Merged by');
});
- describe('template', () => {
- it('should have correct elements', () => {
- const el = createComponent().$el;
+ it('renders author', () => {
+ expect(vm.$el.textContent).toContain('Administrator');
+ });
- expect(el.tagName).toEqual('H4');
- expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl);
- expect(el.querySelector('time').innerText).toContain(props.dateReadable);
- expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle);
- });
+ it('renders provided time', () => {
+ expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z');
+ expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago');
});
});
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 06f89fabf42..13e5595bbfc 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
@@ -1,104 +1,219 @@
import Vue from 'vue';
-import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header';
-
-const createComponent = (mr) => {
- const Component = Vue.extend(headerComponent);
- return new Component({
- el: document.createElement('div'),
- propsData: { mr },
- });
-};
+import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetHeader', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr } = headerComponent.props;
+ let vm;
+ let Component;
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
- });
+ beforeEach(() => {
+ Component = Vue.extend(headerComponent);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
- let vm;
- beforeEach(() => {
- vm = createComponent({
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: '/foo/bar/mr-widget-refactor',
- targetBranch: 'master',
+ describe('shouldShowCommitsBehindText', () => {
+ it('return true when there are divergedCommitsCount', () => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ } });
+
+ expect(vm.shouldShowCommitsBehindText).toEqual(true);
+ });
+
+ it('returns false where there are no divergedComits count', () => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ } });
+ expect(vm.shouldShowCommitsBehindText).toEqual(false);
});
});
- it('shouldShowCommitsBehindText', () => {
- expect(vm.shouldShowCommitsBehindText).toBeTruthy();
+ describe('commitsText', () => {
+ it('returns singular when there is one commit', () => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 1,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ } });
- vm.mr.divergedCommitsCount = 0;
- expect(vm.shouldShowCommitsBehindText).toBeFalsy();
- });
+ expect(vm.commitsText).toEqual('1 commit behind');
+ });
- it('commitsText', () => {
- expect(vm.commitsText).toEqual('commits');
+ it('returns plural when there is more than one commit', () => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 2,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+ targetBranch: 'master',
+ } });
- vm.mr.divergedCommitsCount = 1;
- expect(vm.commitsText).toEqual('commit');
+ expect(vm.commitsText).toEqual('2 commits behind');
+ });
});
});
describe('template', () => {
- let vm;
- let el;
- const sourceBranchPath = '/foo/bar/mr-widget-refactor';
- const mr = {
- divergedCommitsCount: 12,
- sourceBranch: 'mr-widget-refactor',
- sourceBranchLink: `<a href="${sourceBranchPath}">mr-widget-refactor</a>`,
- targetBranchPath: 'foo/bar/commits-path',
- targetBranchTreePath: 'foo/bar/tree/path',
- targetBranch: 'master',
- isOpen: true,
- emailPatchesPath: '/mr/email-patches',
- plainDiffPath: '/mr/plainDiffPath',
- };
-
- beforeEach(() => {
- vm = createComponent(mr);
- el = vm.$el;
+ describe('common elements', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('renders source branch link', () => {
+ expect(
+ vm.$el.querySelector('.js-source-branch').innerHTML,
+ ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
+ });
+
+ it('renders clipboard button', () => {
+ expect(vm.$el.querySelector('.btn-clipboard')).not.toEqual(null);
+ });
+
+ it('renders target branch', () => {
+ expect(vm.$el.querySelector('.js-target-branch').textContent.trim()).toEqual('master');
+ });
+ });
+
+ describe('with an open merge request', () => {
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('renders checkout branch button with modal trigger', () => {
+ const button = vm.$el.querySelector('.js-check-out-branch');
+
+ expect(button.textContent.trim()).toEqual('Check out branch');
+ expect(button.getAttribute('data-target')).toEqual('#modal_merge_info');
+ expect(button.getAttribute('data-toggle')).toEqual('modal');
+ });
+
+ 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');
+ });
});
- it('should render template elements correctly', () => {
- expect(el.classList.contains('mr-source-target')).toBeTruthy();
- const sourceBranchLink = el.querySelectorAll('.label-branch')[0];
- const targetBranchLink = el.querySelectorAll('.label-branch')[1];
- const commitsCount = el.querySelector('.diverged-commits-count');
-
- expect(sourceBranchLink.textContent).toContain(mr.sourceBranch);
- expect(targetBranchLink.textContent).toContain(mr.targetBranch);
- expect(sourceBranchLink.querySelector('a').getAttribute('href')).toEqual(sourceBranchPath);
- expect(targetBranchLink.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchTreePath);
- expect(commitsCount.textContent).toContain('12 commits behind');
- expect(commitsCount.querySelector('a').getAttribute('href')).toEqual(mr.targetBranchPath);
-
- expect(el.textContent).toContain('Check out branch');
- expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath);
- expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath);
+ describe('with a closed merge request', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: false,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('does not render checkout branch button with modal trigger', () => {
+ const button = vm.$el.querySelector('.js-check-out-branch');
+
+ 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('should not have right action links if the MR state is not open', (done) => {
- vm.mr.isOpen = false;
- Vue.nextTick(() => {
- expect(el.textContent).not.toContain('Check out branch');
- expect(el.querySelectorAll('.dropdown li a').length).toEqual(0);
- done();
+ describe('without diverged commits', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 0,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('does not render diverged commits info', () => {
+ expect(vm.$el.querySelector('.diverged-commits-count')).toEqual(null);
});
});
- it('should not render diverged commits count if the MR has no diverged commits', (done) => {
- vm.mr.divergedCommitsCount = null;
- Vue.nextTick(() => {
- expect(el.textContent).not.toContain('commits behind');
- expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0);
- done();
+ describe('with diverged commits', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, { mr: {
+ divergedCommitsCount: 12,
+ sourceBranch: 'mr-widget-refactor',
+ sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+ sourceBranchRemoved: false,
+ targetBranchPath: 'foo/bar/commits-path',
+ targetBranchTreePath: 'foo/bar/tree/path',
+ targetBranch: 'master',
+ isOpen: true,
+ emailPatchesPath: '/mr/email-patches',
+ plainDiffPath: '/mr/plainDiffPath',
+ } });
+ });
+
+ it('renders diverged commits info', () => {
+ expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
index 4da4fc82c26..cc43639f576 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
@@ -1,51 +1,56 @@
import Vue from 'vue';
-import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help';
-
-const props = {
- missingBranch: 'this-is-not-the-branch-you-are-looking-for',
-};
-const text = `If the ${props.missingBranch} branch exists in your local repository`;
-
-const createComponent = () => {
- const Component = Vue.extend(mergeHelpComponent);
- return new Component({
- el: document.createElement('div'),
- propsData: props,
- });
-};
+import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
describe('MRWidgetMergeHelp', () => {
- describe('props', () => {
- it('should have props', () => {
- const { missingBranch } = mergeHelpComponent.props;
- const MissingBranchTypeClass = missingBranch.type;
-
- expect(new MissingBranchTypeClass() instanceof String).toBeTruthy();
- expect(missingBranch.required).toBeFalsy();
- expect(missingBranch.default).toEqual('');
- });
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(mergeHelpComponent);
});
- describe('template', () => {
- let vm;
- let el;
+ afterEach(() => {
+ vm.$destroy();
+ });
+ describe('with missing branch', () => {
beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
+ vm = mountComponent(Component, {
+ missingBranch: 'this-is-not-the-branch-you-are-looking-for',
+ });
});
- it('should have the correct elements', () => {
- expect(el.classList.contains('mr-widget-help')).toBeTruthy();
- expect(el.textContent).toContain(text);
+ it('renders missing branch information', () => {
+ expect(
+ vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '),
+ ).toEqual(
+ 'If the this-is-not-the-branch-you-are-looking-for branch exists in your local repository, you can merge this merge request manually using the command line',
+ );
});
- it('should not show missing branch name if missingBranch props is not provided', (done) => {
- vm.missingBranch = null;
- Vue.nextTick(() => {
- expect(el.textContent).not.toContain(text);
- done();
- });
+ it('renders button to open help modal', () => {
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info');
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal');
+ });
+ });
+
+ describe('without missing branch', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component);
+ });
+
+ it('renders information about how to merge manually', () => {
+ expect(
+ vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ').replace(/\s\s+/g, ' '),
+ ).toEqual(
+ 'You can merge this merge request manually using the command line',
+ );
+ });
+
+ it('renders element to open a modal', () => {
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-target')).toEqual('#modal_merge_info');
+ expect(vm.$el.querySelector('.js-open-modal-help').getAttribute('data-toggle')).toEqual('modal');
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
index f86fb6a0b4b..637bf483deb 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
@@ -1,117 +1,82 @@
import Vue from 'vue';
-import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links';
+import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
-const createComponent = (data) => {
- const Component = Vue.extend(relatedLinksComponent);
+describe('MRWidgetRelatedLinks', () => {
+ let vm;
- return new Component({
- el: document.createElement('div'),
- propsData: data,
- });
-};
+ const createComponent = (data) => {
+ const Component = Vue.extend(relatedLinksComponent);
-describe('MRWidgetRelatedLinks', () => {
- describe('props', () => {
- it('should have props', () => {
- const { relatedLinks } = relatedLinksComponent.props;
+ return mountComponent(Component, data);
+ };
- expect(relatedLinks).toBeDefined();
- expect(relatedLinks.type instanceof Object).toBeTruthy();
- expect(relatedLinks.required).toBeTruthy();
- });
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
- const data = {
- relatedLinks: {
- closing: '/foo',
- mentioned: '/foo',
- assignToMe: '/foo',
- },
- };
-
- describe('hasLinks', () => {
- it('should return correct value when we have links reference', () => {
- const vm = createComponent(data);
- expect(vm.hasLinks).toBeTruthy();
-
- vm.relatedLinks.closing = null;
- expect(vm.hasLinks).toBeTruthy();
-
- vm.relatedLinks.mentioned = null;
- expect(vm.hasLinks).toBeTruthy();
-
- vm.relatedLinks.assignToMe = null;
- expect(vm.hasLinks).toBeFalsy();
- });
- });
-
describe('closesText', () => {
- it('returns correct text for open merge request', () => {
- data.state = 'open';
- const vm = createComponent(data);
+ it('returns Closes text for open merge request', () => {
+ vm = createComponent({ state: 'open', relatedLinks: {} });
expect(vm.closesText).toEqual('Closes');
});
it('returns correct text for closed merge request', () => {
- data.state = 'closed';
- const vm = createComponent(data);
+ vm = createComponent({ state: 'closed', relatedLinks: {} });
expect(vm.closesText).toEqual('Did not close');
});
it('returns correct tense for merged request', () => {
- data.state = 'merged';
- const vm = createComponent(data);
+ vm = createComponent({ state: 'merged', relatedLinks: {} });
expect(vm.closesText).toEqual('Closed');
});
});
});
- describe('template', () => {
- it('should have only have closing issues text', () => {
- const vm = createComponent({
- relatedLinks: {
- closing: '<a href="#">#23</a> and <a>#42</a>',
- },
- });
- const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
-
- expect(content).toContain('Closes #23 and #42');
- expect(content).not.toContain('Mentions');
+ it('should have only have closing issues text', () => {
+ vm = createComponent({
+ relatedLinks: {
+ closing: '<a href="#">#23</a> and <a>#42</a>',
+ },
});
+ const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
- it('should have only have mentioned issues text', () => {
- const vm = createComponent({
- relatedLinks: {
- mentioned: '<a href="#">#7</a>',
- },
- });
+ expect(content).toContain('Closes #23 and #42');
+ expect(content).not.toContain('Mentions');
+ });
- expect(vm.$el.innerText).toContain('Mentions #7');
- expect(vm.$el.innerText).not.toContain('Closes');
+ it('should have only have mentioned issues text', () => {
+ vm = createComponent({
+ relatedLinks: {
+ mentioned: '<a href="#">#7</a>',
+ },
});
- it('should have closing and mentioned issues at the same time', () => {
- const vm = createComponent({
- relatedLinks: {
- closing: '<a href="#">#7</a>',
- mentioned: '<a href="#">#23</a> and <a>#42</a>',
- },
- });
- const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
+ expect(vm.$el.innerText).toContain('Mentions #7');
+ expect(vm.$el.innerText).not.toContain('Closes');
+ });
- expect(content).toContain('Closes #7');
- expect(content).toContain('Mentions #23 and #42');
+ it('should have closing and mentioned issues at the same time', () => {
+ vm = createComponent({
+ relatedLinks: {
+ closing: '<a href="#">#7</a>',
+ mentioned: '<a href="#">#23</a> and <a>#42</a>',
+ },
});
+ const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim();
- it('should have assing issues link', () => {
- const vm = createComponent({
- relatedLinks: {
- assignToMe: '<a href="#">Assign yourself to these issues</a>',
- },
- });
+ expect(content).toContain('Closes #7');
+ expect(content).toContain('Mentions #23 and #42');
+ });
- expect(vm.$el.innerText).toContain('Assign yourself to these issues');
+ it('should have assing issues link', () => {
+ vm = createComponent({
+ relatedLinks: {
+ assignToMe: '<a href="#">Assign yourself to these issues</a>',
+ },
});
+
+ expect(vm.$el.innerText).toContain('Assign yourself to these issues');
});
});
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
new file mode 100644
index 00000000000..c39fcda0071
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
@@ -0,0 +1,44 @@
+import Vue from 'vue';
+import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('MR widget status icon component', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(mrStatusIcon);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('while loading', () => {
+ it('renders loading icon', () => {
+ vm = mountComponent(Component, { status: 'loading' });
+ expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner');
+ });
+ });
+
+ describe('with status icon', () => {
+ it('renders ci status icon', () => {
+ vm = mountComponent(Component, { status: 'failed' });
+ expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull();
+ });
+ });
+
+ describe('with disabled button', () => {
+ it('renders a disabled button', () => {
+ vm = mountComponent(Component, { status: 'failed', showDisabledButton: true });
+ expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge');
+ });
+ });
+
+ describe('without disabled button', () => {
+ it('does not render a disabled button', () => {
+ vm = mountComponent(Component, { status: 'failed' });
+ expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull();
+ });
+ });
+});
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 6b7aa935ad3..658cadddb81 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
@@ -1,19 +1,29 @@
import Vue from 'vue';
-import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking';
+import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetChecking', () => {
- describe('template', () => {
- it('should have correct elements', () => {
- const Component = Vue.extend(checkingComponent);
- const el = new Component({
- el: document.createElement('div'),
- }).$el;
+ let Component;
+ let vm;
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
- expect(el.querySelector('button').disabled).toBeTruthy();
- expect(el.innerText).toContain('Checking ability to merge automatically');
- expect(el.querySelector('i')).toBeDefined();
- });
+ beforeEach(() => {
+ Component = Vue.extend(checkingComponent);
+ vm = mountComponent(Component);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders disabled button', () => {
+ expect(vm.$el.querySelector('button').getAttribute('disabled')).toEqual('disabled');
+ });
+
+ it('renders loading icon', () => {
+ expect(vm.$el.querySelector('.mr-widget-icon i').classList).toContain('fa-spinner');
+ });
+
+ it('renders information about merging', () => {
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toEqual('Checking ability to merge automatically');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
index 1bf97bbf093..51a34739ee9 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -1,74 +1,58 @@
import Vue from 'vue';
-import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed';
-
-const mr = {
- targetBranch: 'good-branch',
- targetBranchPath: '/good-branch',
- metrics: {
- mergedBy: {},
- mergedAt: 'mergedUpdatedAt',
- closedBy: {
- name: 'Fatih Acet',
- username: 'fatihacet',
- },
- closedAt: 'closedEventUpdatedAt',
- readableMergedAt: '',
- readableClosedAt: '',
- },
- updatedAt: 'mrUpdatedAt',
- closedAt: '1 day ago',
-};
-
-const createComponent = () => {
- const Component = Vue.extend(closedComponent);
-
- return new Component({
- el: document.createElement('div'),
- propsData: { mr },
- });
-};
+import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetClosed', () => {
- describe('props', () => {
- it('should have props', () => {
- const mrProp = closedComponent.props.mr;
-
- expect(mrProp.type instanceof Object).toBeTruthy();
- expect(mrProp.required).toBeTruthy();
- });
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(closedComponent);
+ vm = mountComponent(Component, { mr: {
+ metrics: {
+ mergedBy: {},
+ closedBy: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ mergedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ closedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ readableMergedAt: '',
+ readableClosedAt: 'less than a minute ago',
+ },
+ targetBranchPath: '/twitter/flight/commits/so_long_jquery',
+ targetBranch: 'so_long_jquery',
+ } });
});
- describe('components', () => {
- it('should have components added', () => {
- expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined();
- });
+ afterEach(() => {
+ vm.$destroy();
});
- describe('template', () => {
- let vm;
- let el;
+ it('renders warning icon', () => {
+ expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
+ });
- beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
- });
+ it('renders closed by information with author and time', () => {
+ expect(
+ vm.$el.querySelector('.js-mr-widget-author').textContent.trim().replace(/\s\s+/g, ' '),
+ ).toContain(
+ 'Closed by Administrator less than a minute ago',
+ );
+ });
- afterEach(() => {
- vm.$destroy();
- });
+ it('links to the user that closed the MR', () => {
+ expect(vm.$el.querySelector('.author-link').getAttribute('href')).toEqual('http://localhost:3000/root');
+ });
- it('should have correct elements', () => {
- expect(el.querySelector('h4').textContent).toContain('Closed by');
- expect(el.querySelector('h4').textContent).toContain(mr.metrics.closedBy.name);
- expect(el.textContent).toContain('The changes were not merged into');
- expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
- expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
- });
+ it('renders information about the changes not being merged', () => {
+ expect(
+ vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' '),
+ ).toContain('The changes were not merged into so_long_jquery');
+ });
- it('should use closedEvent updatedAt as tooltip title', () => {
- expect(
- el.querySelector('time').getAttribute('title'),
- ).toBe('closedEventUpdatedAt');
- });
+ it('renders link for target branch', () => {
+ expect(vm.$el.querySelector('.label-branch').getAttribute('href')).toEqual('/twitter/flight/commits/so_long_jquery');
});
});
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 5d4c7ec09dc..a7d69fdcdb9 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,105 +1,85 @@
import Vue from 'vue';
-import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts';
+import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
-const ConflictsComponent = Vue.extend(conflictsComponent);
-const path = '/conflicts';
-
describe('MRWidgetConflicts', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr } = conflictsComponent.props;
+ let Component;
+ let vm;
+ const path = '/conflicts';
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
- });
+ beforeEach(() => {
+ Component = Vue.extend(conflictsComponent);
});
- describe('template', () => {
- describe('when allowed to merge', () => {
- let vm;
-
- beforeEach(() => {
- vm = mountComponent(ConflictsComponent, {
- mr: {
- canMerge: true,
- conflictResolutionPath: path,
- },
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('should tell you about conflicts without bothering other people', () => {
- expect(vm.$el.textContent).toContain('There are merge conflicts');
- expect(vm.$el.textContent).not.toContain('ask someone with write access');
- });
-
- it('should allow you to resolve the conflicts', () => {
- const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button');
+ afterEach(() => {
+ vm.$destroy();
+ });
- expect(resolveButton.textContent).toContain('Resolve conflicts');
- expect(resolveButton.getAttribute('href')).toEqual(path);
+ describe('when allowed to merge', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ canMerge: true,
+ conflictResolutionPath: path,
+ },
});
+ });
- it('should have merge buttons', () => {
- const mergeButton = vm.$el.querySelector('.js-disabled-merge-button');
- const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button');
-
- expect(mergeButton.textContent).toContain('Merge');
- expect(mergeButton.disabled).toBeTruthy();
- expect(mergeButton.classList.contains('btn-success')).toEqual(true);
- expect(mergeLocallyButton.textContent).toContain('Merge locally');
- });
+ it('should tell you about conflicts without bothering other people', () => {
+ expect(vm.$el.textContent).toContain('There are merge conflicts');
+ expect(vm.$el.textContent).not.toContain('ask someone with write access');
});
- describe('when user does not have permission to merge', () => {
- let vm;
+ it('should allow you to resolve the conflicts', () => {
+ const resolveButton = vm.$el.querySelector('.js-resolve-conflicts-button');
- beforeEach(() => {
- vm = mountComponent(ConflictsComponent, {
- mr: {
- canMerge: false,
- },
- });
- });
+ expect(resolveButton.textContent).toContain('Resolve conflicts');
+ expect(resolveButton.getAttribute('href')).toEqual(path);
+ });
- afterEach(() => {
- vm.$destroy();
- });
+ it('should have merge buttons', () => {
+ const mergeButton = vm.$el.querySelector('.js-disabled-merge-button');
+ const mergeLocallyButton = vm.$el.querySelector('.js-merge-locally-button');
- it('should show proper message', () => {
- expect(vm.$el.textContent).toContain('ask someone with write access');
- });
+ expect(mergeButton.textContent).toContain('Merge');
+ expect(mergeButton.disabled).toBeTruthy();
+ expect(mergeButton.classList.contains('btn-success')).toEqual(true);
+ expect(mergeLocallyButton.textContent).toContain('Merge locally');
+ });
+ });
- it('should not have action buttons', () => {
- expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined();
- expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull();
- expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull();
+ describe('when user does not have permission to merge', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ canMerge: false,
+ },
});
});
- describe('when fast-forward or semi-linear merge enabled', () => {
- let vm;
+ it('should show proper message', () => {
+ expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('ask someone with write access');
+ });
- beforeEach(() => {
- vm = mountComponent(ConflictsComponent, {
- mr: {
- shouldBeRebased: true,
- },
- });
- });
+ it('should not have action buttons', () => {
+ expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeDefined();
+ expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toBeNull();
+ expect(vm.$el.querySelector('.js-merge-locally-button')).toBeNull();
+ });
+ });
- afterEach(() => {
- vm.$destroy();
+ describe('when fast-forward or semi-linear merge enabled', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ mr: {
+ shouldBeRebased: true,
+ },
});
+ });
- it('should tell you to rebase locally', () => {
- expect(vm.$el.textContent).toContain('Fast-forward merge is not possible.');
- expect(vm.$el.textContent).toContain('To merge this request, first rebase locally');
- });
+ it('should tell you to rebase locally', () => {
+ expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('Fast-forward merge is not possible.');
+ expect(vm.$el.textContent.trim().replace(/\s\s+/g, ' ')).toContain('To merge this request, first rebase locally');
});
});
});
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 cef365eec8a..a57b9811e08 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
@@ -1,45 +1,37 @@
import Vue from 'vue';
-import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge';
+import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-
-const mr = {
- mergeError: 'Merge error happened.',
-};
-const createComponent = () => {
- const Component = Vue.extend(failedToMergeComponent);
- return new Component({
- el: document.createElement('div'),
- propsData: { mr },
- });
-};
+import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetFailedToMerge', () => {
- describe('data', () => {
- it('should have default data', () => {
- const data = failedToMergeComponent.data();
+ let Component;
+ let vm;
+
+ beforeEach(() => {
+ Component = Vue.extend(failedToMergeComponent);
+ spyOn(eventHub, '$emit');
+ vm = mountComponent(Component, { mr: {
+ mergeError: 'Merge error happened.',
+ } });
+ });
- expect(data.timer).toEqual(10);
- expect(data.isRefreshing).toBeFalsy();
- });
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
describe('timerText', () => {
it('should return correct timer text', () => {
- const vm = createComponent();
- expect(vm.timerText).toEqual('10 seconds');
+ expect(vm.timerText).toEqual('Refreshing in 10 seconds to show the updated status...');
vm.timer = 1;
- expect(vm.timerText).toEqual('a second');
+ expect(vm.timerText).toEqual('Refreshing in a second to show the updated status...');
});
});
});
describe('created', () => {
it('should disable polling', () => {
- spyOn(eventHub, '$emit');
- createComponent();
-
expect(eventHub.$emit).toHaveBeenCalledWith('DisablePolling');
});
});
@@ -47,13 +39,10 @@ describe('MRWidgetFailedToMerge', () => {
describe('methods', () => {
describe('refresh', () => {
it('should emit event to request component refresh', () => {
- spyOn(eventHub, '$emit');
- const vm = createComponent();
-
- expect(vm.isRefreshing).toBeFalsy();
+ expect(vm.isRefreshing).toEqual(false);
vm.refresh();
- expect(vm.isRefreshing).toBeTruthy();
+ expect(vm.isRefreshing).toEqual(true);
expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
expect(eventHub.$emit).toHaveBeenCalledWith('EnablePolling');
});
@@ -61,12 +50,11 @@ describe('MRWidgetFailedToMerge', () => {
describe('updateTimer', () => {
it('should update timer and emit event when timer end', () => {
- const vm = createComponent();
spyOn(vm, 'refresh');
expect(vm.timer).toEqual(10);
- for (let i = 0; i < 10; i++) { // eslint-disable-line
+ for (let i = 0; i < 10; i += 1) {
expect(vm.timer).toEqual(10 - i);
vm.updateTimer();
}
@@ -76,47 +64,54 @@ describe('MRWidgetFailedToMerge', () => {
});
});
- describe('template', () => {
- let vm;
- let el;
+ describe('while it is refreshing', () => {
+ it('renders Refresing now', (done) => {
+ vm.isRefreshing = true;
- beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.js-refresh-label').textContent.trim()).toEqual('Refreshing now');
+ done();
+ });
});
+ });
- it('should have correct elements', (done) => {
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('Merge error happened.');
- expect(el.innerText).toContain('Refreshing in 10 seconds');
- expect(el.innerText).not.toContain('Merge failed.');
- expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(el.querySelector('button').innerText).toContain('Merge');
- expect(el.querySelector('.js-refresh-button').innerText).toContain('Refresh now');
- expect(el.querySelector('.js-refresh-label')).toEqual(null);
- expect(el.innerText).not.toContain('Refreshing now');
- setTimeout(() => {
- expect(el.innerText).toContain('Refreshing in 9 seconds');
- done();
- }, 1010);
+ describe('while it is not regresing', () => {
+ it('renders warning icon and disabled merge button', () => {
+ expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual('disabled');
+ });
+
+ it('renders given error', () => {
+ expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual('Merge error happened..');
});
- it('should just generic merge failed message if merge_error is not available', (done) => {
- vm.mr.mergeError = null;
+ it('renders refresh button', () => {
+ expect(vm.$el.querySelector('.js-refresh-button').textContent.trim()).toEqual('Refresh now');
+ });
- Vue.nextTick(() => {
- expect(el.innerText).toContain('Merge failed.');
- expect(el.innerText).not.toContain('Merge error happened.');
- done();
- });
+ it('renders remaining time', () => {
+ expect(
+ vm.$el.querySelector('.has-custom-error').textContent.trim(),
+ ).toEqual('Refreshing in 10 seconds to show the updated status...');
+ });
+ });
+
+ it('should just generic merge failed message if merge_error is not available', (done) => {
+ vm.mr.mergeError = null;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).toContain('Merge failed.');
+ expect(vm.$el.innerText).not.toContain('Merge error happened.');
+ done();
});
+ });
- it('should show refresh label when refresh requested', () => {
- vm.refresh();
- Vue.nextTick(() => {
- expect(el.innerText).not.toContain('Merge failed. Refreshing');
- expect(el.innerText).toContain('Refreshing now');
- });
+ it('should show refresh label when refresh requested', (done) => {
+ vm.refresh();
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).not.toContain('Merge failed. Refreshing');
+ expect(vm.$el.innerText).toContain('Refreshing now');
+ done();
});
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
deleted file mode 100644
index 237035648cf..00000000000
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Vue from 'vue';
-import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging';
-
-describe('MRWidgetMerging', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr } = mergingComponent.props;
-
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
- });
- });
-
- describe('template', () => {
- it('should have correct elements', () => {
- const Component = Vue.extend(mergingComponent);
- const mr = {
- targetBranchPath: '/branch-path',
- targetBranch: 'branch',
- };
- const el = new Component({
- el: document.createElement('div'),
- propsData: { mr },
- }).$el;
-
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('This merge request is in the process of being merged');
- expect(el.innerText).toContain('changes will be merged into');
- expect(el.querySelector('.label-branch a').getAttribute('href')).toEqual(mr.targetBranchPath);
- expect(el.querySelector('.label-branch a').textContent).toContain(mr.targetBranch);
- });
- });
-});
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 5f4df15bcd6..df56c4e2c5c 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
@@ -1,77 +1,50 @@
import Vue from 'vue';
-import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds';
+import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-
-const targetBranchPath = '/foo/bar';
-const targetBranch = 'foo';
-const sha = '1EA2EZ34';
-
-const createComponent = () => {
- const Component = Vue.extend(mwpsComponent);
- const mr = {
- shouldRemoveSourceBranch: false,
- canRemoveSourceBranch: true,
- canCancelAutomaticMerge: true,
- mergeUserId: 1,
- currentUserId: 1,
- setToMWPSBy: {},
- sha,
- targetBranchPath,
- targetBranch,
- };
-
- const service = {
- cancelAutomaticMerge() {},
- mergeResource: {
- save() {},
- },
- };
-
- return new Component({
- el: document.createElement('div'),
- propsData: { mr, service },
- });
-};
+import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetMergeWhenPipelineSucceeds', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr, service } = mwpsComponent.props;
-
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
-
- expect(service.type instanceof Object).toBeTruthy();
- expect(service.required).toBeTruthy();
+ let vm;
+ const targetBranchPath = '/foo/bar';
+ const targetBranch = 'foo';
+ const sha = '1EA2EZ34';
+
+ beforeEach(() => {
+ const Component = Vue.extend(mwpsComponent);
+ spyOn(eventHub, '$emit');
+
+ vm = mountComponent(Component, {
+ mr: {
+ shouldRemoveSourceBranch: false,
+ canRemoveSourceBranch: true,
+ canCancelAutomaticMerge: true,
+ mergeUserId: 1,
+ currentUserId: 1,
+ setToMWPSBy: {},
+ sha,
+ targetBranchPath,
+ targetBranch,
+ },
+ service: {
+ cancelAutomaticMerge() {},
+ mergeResource: {
+ save() {},
+ },
+ },
});
});
- describe('components', () => {
- it('should have components added', () => {
- expect(mwpsComponent.components['mr-widget-author']).toBeDefined();
- });
- });
-
- describe('data', () => {
- it('should have default data', () => {
- const data = mwpsComponent.data();
-
- expect(data.isCancellingAutoMerge).toBeFalsy();
- expect(data.isRemovingSourceBranch).toBeFalsy();
- });
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
describe('canRemoveSourceBranch', () => {
it('should return true when user is able to remove source branch', () => {
- const vm = createComponent();
-
expect(vm.canRemoveSourceBranch).toBeTruthy();
});
it('should return false when user id is not the same with who set the MWPS', () => {
- const vm = createComponent();
-
vm.mr.mergeUserId = 2;
expect(vm.canRemoveSourceBranch).toBeFalsy();
@@ -83,15 +56,11 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
});
it('should return false when shouldRemoveSourceBranch set to false', () => {
- const vm = createComponent();
-
vm.mr.shouldRemoveSourceBranch = true;
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
it('should return false if user is not able to remove the source branch', () => {
- const vm = createComponent();
-
vm.mr.canRemoveSourceBranch = false;
expect(vm.canRemoveSourceBranch).toBeFalsy();
});
@@ -101,11 +70,9 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
describe('methods', () => {
describe('cancelAutomaticMerge', () => {
it('should set flag and call service then tell main component to update the widget with data', (done) => {
- const vm = createComponent();
const mrObj = {
is_new_mr_data: true,
};
- spyOn(eventHub, '$emit');
spyOn(vm.service, 'cancelAutomaticMerge').and.returnValue(new Promise((resolve) => {
resolve({
data: mrObj,
@@ -123,8 +90,6 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', (done) => {
- const vm = createComponent();
- spyOn(eventHub, '$emit');
spyOn(vm.service.mergeResource, 'save').and.returnValue(new Promise((resolve) => {
resolve({
data: {
@@ -148,31 +113,23 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
});
describe('template', () => {
- let vm;
- let el;
-
- beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
- });
-
it('should have correct elements', () => {
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds');
- expect(el.innerText).toContain('The changes will be merged into');
- expect(el.innerText).toContain(targetBranch);
- expect(el.innerText).toContain('The source branch will not be removed');
- expect(el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge');
- expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy();
- expect(el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch');
- expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy();
+ expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
+ expect(vm.$el.innerText).toContain('to be merged automatically when the pipeline succeeds');
+ expect(vm.$el.innerText).toContain('The changes will be merged into');
+ expect(vm.$el.innerText).toContain(targetBranch);
+ expect(vm.$el.innerText).toContain('The source branch will not be removed');
+ expect(vm.$el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge');
+ expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy();
+ expect(vm.$el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch');
+ expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeFalsy();
});
it('should disable cancel auto merge button when the action is in progress', (done) => {
vm.isCancellingAutoMerge = true;
Vue.nextTick(() => {
- expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeTruthy();
done();
});
});
@@ -181,7 +138,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
vm.mr.shouldRemoveSourceBranch = true;
Vue.nextTick(() => {
- const normalizedText = el.innerText.replace(/\s+/g, ' ');
+ const normalizedText = vm.$el.innerText.replace(/\s+/g, ' ');
expect(normalizedText).toContain('The source branch will be removed');
expect(normalizedText).not.toContain('The source branch will not be removed');
done();
@@ -192,7 +149,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
vm.mr.currentUserId = 4;
Vue.nextTick(() => {
- expect(el.querySelector('.js-remove-source-branch')).toEqual(null);
+ expect(vm.$el.querySelector('.js-remove-source-branch')).toEqual(null);
done();
});
});
@@ -201,7 +158,7 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => {
vm.isRemovingSourceBranch = true;
Vue.nextTick(() => {
- expect(el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy();
+ expect(vm.$el.querySelector('.js-remove-source-branch').getAttribute('disabled')).toBeTruthy();
done();
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 2dc3b72ea40..43a989393ba 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -1,108 +1,99 @@
import Vue from 'vue';
-import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged';
+import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-
-const targetBranch = 'foo';
-
-const createComponent = () => {
- const Component = Vue.extend(mergedComponent);
- const mr = {
- isRemovingSourceBranch: false,
- cherryPickInForkPath: false,
- canCherryPickInCurrentMR: true,
- revertInForkPath: false,
- canRevertInCurrentMR: true,
- canRemoveSourceBranch: true,
- sourceBranchRemoved: true,
- metrics: {
- mergedBy: {},
- mergedAt: 'mergedUpdatedAt',
- readableMergedAt: '',
- closedBy: {},
- closedAt: 'mergedUpdatedAt',
- readableClosedAt: '',
- },
- updatedAt: 'mrUpdatedAt',
- targetBranch,
- };
-
- const service = {
- removeSourceBranch() {},
- };
-
- return new Component({
- el: document.createElement('div'),
- propsData: { mr, service },
- });
-};
+import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetMerged', () => {
- describe('props', () => {
- it('should have props', () => {
- const { mr, service } = mergedComponent.props;
-
- expect(mr.type instanceof Object).toBeTruthy();
- expect(mr.required).toBeTruthy();
-
- expect(service.type instanceof Object).toBeTruthy();
- expect(service.required).toBeTruthy();
- });
- });
-
- describe('components', () => {
- it('should have components added', () => {
- expect(mergedComponent.components['mr-widget-author-and-time']).toBeDefined();
- });
+ let vm;
+ const targetBranch = 'foo';
+
+ beforeEach(() => {
+ const Component = Vue.extend(mergedComponent);
+ const mr = {
+ isRemovingSourceBranch: false,
+ cherryPickInForkPath: false,
+ canCherryPickInCurrentMR: true,
+ revertInForkPath: false,
+ canRevertInCurrentMR: true,
+ canRemoveSourceBranch: true,
+ sourceBranchRemoved: true,
+ metrics: {
+ mergedBy: {
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://localhost:3000/root',
+ avatarUrl: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ },
+ mergedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ readableMergedAt: '',
+ closedBy: {},
+ closedAt: 'Jan 24, 2018 1:02pm GMT+0000',
+ readableClosedAt: '',
+ },
+ updatedAt: 'mergedUpdatedAt',
+ targetBranch,
+ };
+
+ const service = {
+ removeSourceBranch() {},
+ };
+
+ spyOn(eventHub, '$emit');
+
+ vm = mountComponent(Component, { mr, service });
});
- describe('data', () => {
- it('should have default data', () => {
- const data = mergedComponent.data();
-
- expect(data.isMakingRequest).toBeFalsy();
- });
+ afterEach(() => {
+ vm.$destroy();
});
describe('computed', () => {
describe('shouldShowRemoveSourceBranch', () => {
- it('should correct value when fields changed', () => {
- const vm = createComponent();
+ it('returns true when sourceBranchRemoved is false', () => {
vm.mr.sourceBranchRemoved = false;
- expect(vm.shouldShowRemoveSourceBranch).toBeTruthy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(true);
+ });
+ it('returns false wehn sourceBranchRemoved is true', () => {
vm.mr.sourceBranchRemoved = true;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+ it('returns false when canRemoveSourceBranch is false', () => {
vm.mr.sourceBranchRemoved = false;
vm.mr.canRemoveSourceBranch = false;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+ it('returns false when is making request', () => {
vm.mr.canRemoveSourceBranch = true;
vm.isMakingRequest = true;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
+ });
+ it('returns true when all are true', () => {
vm.mr.isRemovingSourceBranch = true;
vm.mr.canRemoveSourceBranch = true;
vm.isMakingRequest = true;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
});
});
+
describe('shouldShowSourceBranchRemoving', () => {
it('should correct value when fields changed', () => {
- const vm = createComponent();
vm.mr.sourceBranchRemoved = false;
- expect(vm.shouldShowSourceBranchRemoving).toBeFalsy();
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(false);
vm.mr.sourceBranchRemoved = true;
- expect(vm.shouldShowRemoveSourceBranch).toBeFalsy();
+ expect(vm.shouldShowRemoveSourceBranch).toEqual(false);
vm.mr.sourceBranchRemoved = false;
vm.isMakingRequest = true;
- expect(vm.shouldShowSourceBranchRemoving).toBeTruthy();
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
vm.isMakingRequest = false;
vm.mr.isRemovingSourceBranch = true;
- expect(vm.shouldShowSourceBranchRemoving).toBeTruthy();
+ expect(vm.shouldShowSourceBranchRemoving).toEqual(true);
});
});
});
@@ -110,8 +101,6 @@ describe('MRWidgetMerged', () => {
describe('methods', () => {
describe('removeSourceBranch', () => {
it('should set flag and call service then request main component to update the widget', (done) => {
- const vm = createComponent();
- spyOn(eventHub, '$emit');
spyOn(vm.service, 'removeSourceBranch').and.returnValue(new Promise((resolve) => {
resolve({
data: {
@@ -123,7 +112,7 @@ describe('MRWidgetMerged', () => {
vm.removeSourceBranch();
setTimeout(() => {
const args = eventHub.$emit.calls.argsFor(0);
- expect(vm.isMakingRequest).toBeTruthy();
+ expect(vm.isMakingRequest).toEqual(true);
expect(args[0]).toEqual('MRWidgetUpdateRequested');
expect(args[1]).not.toThrow();
done();
@@ -132,53 +121,50 @@ describe('MRWidgetMerged', () => {
});
});
- describe('template', () => {
- let vm;
- let el;
+ it('has merged by information', () => {
+ expect(vm.$el.textContent).toContain('Merged by');
+ expect(vm.$el.textContent).toContain('Administrator');
+ });
- beforeEach(() => {
- vm = createComponent();
- el = vm.$el;
- });
+ it('renders branch information', () => {
+ expect(vm.$el.textContent).toContain('The changes were merged into');
+ expect(vm.$el.textContent).toContain(targetBranch);
+ });
- it('should have correct elements', () => {
- expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.querySelector('.js-mr-widget-author')).toBeDefined();
- expect(el.innerText).toContain('The changes were merged into');
- expect(el.innerText).toContain(targetBranch);
- expect(el.innerText).toContain('The source branch has been removed');
- expect(el.innerText).toContain('Revert');
- expect(el.innerText).toContain('Cherry-pick');
- expect(el.innerText).not.toContain('You can remove source branch now');
- expect(el.innerText).not.toContain('The source branch is being removed');
- });
+ it('renders information about branch being removed', () => {
+ expect(vm.$el.textContent).toContain('The source branch has been removed');
+ });
- it('should not show source branch removed text', (done) => {
- vm.mr.sourceBranchRemoved = false;
+ it('shows revert and cherry-pick buttons', () => {
+ expect(vm.$el.textContent).toContain('Revert');
+ expect(vm.$el.textContent).toContain('Cherry-pick');
+ });
- Vue.nextTick(() => {
- expect(el.innerText).toContain('You can remove source branch now');
- expect(el.innerText).not.toContain('The source branch has been removed');
- done();
- });
+ it('should not show source branch removed text', (done) => {
+ vm.mr.sourceBranchRemoved = false;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).toContain('You can remove source branch now');
+ expect(vm.$el.innerText).not.toContain('The source branch has been removed');
+ done();
});
+ });
- it('should show source branch removing text', (done) => {
- vm.mr.isRemovingSourceBranch = true;
- vm.mr.sourceBranchRemoved = false;
+ it('should show source branch removing text', (done) => {
+ vm.mr.isRemovingSourceBranch = true;
+ vm.mr.sourceBranchRemoved = false;
- Vue.nextTick(() => {
- expect(el.innerText).toContain('The source branch is being removed');
- expect(el.innerText).not.toContain('You can remove source branch now');
- expect(el.innerText).not.toContain('The source branch has been removed');
- done();
- });
+ Vue.nextTick(() => {
+ expect(vm.$el.innerText).toContain('The source branch is being removed');
+ expect(vm.$el.innerText).not.toContain('You can remove source branch now');
+ expect(vm.$el.innerText).not.toContain('The source branch has been removed');
+ done();
});
+ });
- it('should use mergedEvent updatedAt as tooltip title', () => {
- expect(
- el.querySelector('time').getAttribute('title'),
- ).toBe('mergedUpdatedAt');
- });
+ it('should use mergedEvent mergedAt as tooltip title', () => {
+ expect(
+ vm.$el.querySelector('time').getAttribute('title'),
+ ).toBe('Jan 24, 2018 1:02pm GMT+0000');
});
});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js
new file mode 100644
index 00000000000..0b2ed2d4086
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js
@@ -0,0 +1,34 @@
+import Vue from 'vue';
+import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
+import mountComponent from '../../../helpers/vue_mount_component_helper';
+
+describe('MRWidgetMerging', () => {
+ let vm;
+ beforeEach(() => {
+ const Component = Vue.extend(mergingComponent);
+
+ vm = mountComponent(Component, { mr: {
+ targetBranchPath: '/branch-path',
+ targetBranch: 'branch',
+ } });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders information about merge request being merged', () => {
+ expect(
+ vm.$el.querySelector('.media-body').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '),
+ ).toContain('This merge request is in the process of being merged');
+ });
+
+ it('renders branch information', () => {
+ expect(
+ vm.$el.querySelector('.mr-info-list').textContent.trim().replace(/\s\s+/g, ' ').replace(/[\r\n]+/g, ' '),
+ ).toEqual('The changes will be merged into branch');
+ expect(
+ vm.$el.querySelector('a').getAttribute('href'),
+ ).toEqual('/branch-path');
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index ae494267659..3dd75307484 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -38,7 +38,7 @@ export default {
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"merged_at": "2017-04-07T15:39:25.696Z",
@@ -50,7 +50,7 @@ export default {
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"merge_user": null,
@@ -64,7 +64,7 @@ export default {
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
"active": false,
@@ -159,10 +159,10 @@ export default {
"username": "root",
"id": 1,
"state": "active",
- "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "http://localhost:3000/root"
},
- "author_gravatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d",
"commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d"
},
diff --git a/spec/javascripts/vue_shared/components/confirmation_input_spec.js b/spec/javascripts/vue_shared/components/confirmation_input_spec.js
new file mode 100644
index 00000000000..a6a12614e77
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/confirmation_input_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import confirmationInput from '~/vue_shared/components/confirmation_input.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Confirmation input component', () => {
+ const Component = Vue.extend(confirmationInput);
+ const props = {
+ inputId: 'dummy-id',
+ confirmationKey: 'confirmation-key',
+ confirmationValue: 'confirmation-value',
+ };
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('props', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ it('sets id of the input field to inputId', () => {
+ expect(vm.$refs.enteredValue.id).toBe(props.inputId);
+ });
+
+ it('sets name of the input field to confirmationKey', () => {
+ expect(vm.$refs.enteredValue.name).toBe(props.confirmationKey);
+ });
+ });
+
+ describe('computed', () => {
+ describe('inputLabel', () => {
+ it('escapes confirmationValue by default', () => {
+ vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng' });
+ expect(vm.inputLabel).toBe('Type <code>n&lt;e&gt;&lt;/e&gt;ds escap&quot;ng</code> to confirm:');
+ });
+
+ it('does not escape confirmationValue if escapeValue is false', () => {
+ vm = mountComponent(Component, { ...props, confirmationValue: 'n<e></e>ds escap"ng', shouldEscapeConfirmationValue: false });
+ expect(vm.inputLabel).toBe('Type <code>n<e></e>ds escap"ng</code> to confirm:');
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('hasCorrectValue', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ it('returns false if entered value is incorrect', () => {
+ vm.$refs.enteredValue.value = 'incorrect';
+ expect(vm.hasCorrectValue()).toBe(false);
+ });
+
+ it('returns true if entered value is correct', () => {
+ vm.$refs.enteredValue.value = props.confirmationValue;
+ expect(vm.hasCorrectValue()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/lib/banzai/color_parser_spec.rb b/spec/lib/banzai/color_parser_spec.rb
new file mode 100644
index 00000000000..a1cb0c07b06
--- /dev/null
+++ b/spec/lib/banzai/color_parser_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+describe Banzai::ColorParser do
+ describe '.parse' do
+ context 'HEX format' do
+ [
+ '#abc', '#ABC',
+ '#d2d2d2', '#D2D2D2',
+ '#123a', '#123A',
+ '#123456aa', '#123456AA'
+ ].each do |color|
+ it "parses the valid hex color #{color}" do
+ expect(subject.parse(color)).to eq(color)
+ end
+ end
+
+ [
+ '#', '#1', '#12', '#12g', '#12G',
+ '#12345', '#r2r2r2', '#R2R2R2', '#1234567',
+ '# 123', '# 1234', '# 123456', '# 12345678',
+ '#1 2 3', '#123 4', '#12 34 56', '#123456 78'
+ ].each do |color|
+ it "does not parse the invalid hex color #{color}" do
+ expect(subject.parse(color)).to be_nil
+ end
+ end
+ end
+
+ context 'RGB format' do
+ [
+ 'rgb(0,0,0)', 'rgb(255,255,255)',
+ 'rgb(0, 0, 0)', 'RGB(0,0,0)',
+ 'rgb(0,0,0,0)', 'rgb(0,0,0,0.0)', 'rgb(0,0,0,.0)',
+ 'rgb(0,0,0, 0)', 'rgb(0,0,0, 0.0)', 'rgb(0,0,0, .0)',
+ 'rgb(0,0,0,1)', 'rgb(0,0,0,1.0)',
+ 'rgba(0,0,0)', 'rgba(0,0,0,0)', 'RGBA(0,0,0)',
+ 'rgb(0%,0%,0%)', 'rgba(0%,0%,0%,0%)'
+ ].each do |color|
+ it "parses the valid rgb color #{color}" do
+ expect(subject.parse(color)).to eq(color)
+ end
+ end
+
+ [
+ 'FOOrgb(0,0,0)', 'rgb(0,0,0)BAR',
+ 'rgb(0,0,-1)', 'rgb(0,0,-0)', 'rgb(0,0,256)',
+ 'rgb(0,0,0,-0.1)', 'rgb(0,0,0,-0.0)', 'rgb(0,0,0,-.1)',
+ 'rgb(0,0,0,1.1)', 'rgb(0,0,0,2)',
+ 'rgba(0,0,0,)', 'rgba(0,0,0,0.)', 'rgba(0,0,0,1.)',
+ 'rgb(0,0,0%)', 'rgb(101%,0%,0%)'
+ ].each do |color|
+ it "does not parse the invalid rgb color #{color}" do
+ expect(subject.parse(color)).to be_nil
+ end
+ end
+ end
+
+ context 'HSL format' do
+ [
+ 'hsl(0,0%,0%)', 'hsl(0,100%,100%)',
+ 'hsl(540,0%,0%)', 'hsl(-720,0%,0%)',
+ 'hsl(0deg,0%,0%)', 'hsl(0DEG,0%,0%)',
+ 'hsl(0, 0%, 0%)', 'HSL(0,0%,0%)',
+ 'hsl(0,0%,0%,0)', 'hsl(0,0%,0%,0.0)', 'hsl(0,0%,0%,.0)',
+ 'hsl(0,0%,0%, 0)', 'hsl(0,0%,0%, 0.0)', 'hsl(0,0%,0%, .0)',
+ 'hsl(0,0%,0%,1)', 'hsl(0,0%,0%,1.0)',
+ 'hsla(0,0%,0%)', 'hsla(0,0%,0%,0)', 'HSLA(0,0%,0%)',
+ 'hsl(1rad,0%,0%)', 'hsl(1.1rad,0%,0%)', 'hsl(.1rad,0%,0%)',
+ 'hsl(-1rad,0%,0%)', 'hsl(1RAD,0%,0%)'
+ ].each do |color|
+ it "parses the valid hsl color #{color}" do
+ expect(subject.parse(color)).to eq(color)
+ end
+ end
+
+ [
+ 'hsl(+0,0%,0%)', 'hsl(0,0,0%)', 'hsl(0,0%,0)', 'hsl(0 deg,0%,0%)',
+ 'hsl(0,-0%,0%)', 'hsl(0,101%,0%)', 'hsl(0,-1%,0%)',
+ 'hsl(0,0%,0%,-0.1)', 'hsl(0,0%,0%,-.1)',
+ 'hsl(0,0%,0%,1.1)', 'hsl(0,0%,0%,2)',
+ 'hsl(0,0%,0%,)', 'hsl(0,0%,0%,0.)', 'hsl(0,0%,0%,1.)',
+ 'hsl(deg,0%,0%)', 'hsl(rad,0%,0%)'
+ ].each do |color|
+ it "does not parse the invalid hsl color #{color}" do
+ expect(subject.parse(color)).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/color_filter_spec.rb b/spec/lib/banzai/filter/color_filter_spec.rb
new file mode 100644
index 00000000000..a098b037510
--- /dev/null
+++ b/spec/lib/banzai/filter/color_filter_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Banzai::Filter::ColorFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:color) { '#F00' }
+ let(:color_chip_selector) { 'code > span.gfm-color_chip > span' }
+
+ ['#123', '#1234', '#123456', '#12345678',
+ 'rgb(0,0,0)', 'RGB(0, 0, 0)', 'rgba(0,0,0,1)', 'RGBA(0,0,0,0.7)',
+ 'hsl(270,30%,50%)', 'HSLA(270, 30%, 50%, .7)'].each do |color|
+ it "inserts color chip for supported color format #{color}" do
+ content = code_tag(color)
+ doc = filter(content)
+ color_chip = doc.at_css(color_chip_selector)
+
+ expect(color_chip.content).to be_empty
+ expect(color_chip.parent[:class]).to eq 'gfm-color_chip'
+ expect(color_chip[:style]).to eq "background-color: #{color};"
+ end
+ end
+
+ it 'ignores valid color code without backticks(code tags)' do
+ doc = filter(color)
+
+ expect(doc.css('span.gfm-color_chip').size).to be_zero
+ end
+
+ it 'ignores valid color code with prepended space' do
+ content = code_tag(' ' + color)
+ doc = filter(content)
+
+ expect(doc.css(color_chip_selector).size).to be_zero
+ end
+
+ it 'ignores valid color code with appended space' do
+ content = code_tag(color + ' ')
+ doc = filter(content)
+
+ expect(doc.css(color_chip_selector).size).to be_zero
+ end
+
+ it 'ignores valid color code surrounded by spaces' do
+ content = code_tag(' ' + color + ' ')
+ doc = filter(content)
+
+ expect(doc.css(color_chip_selector).size).to be_zero
+ end
+
+ it 'ignores invalid color code' do
+ invalid_color = '#BAR'
+ content = code_tag(invalid_color)
+ doc = filter(content)
+
+ expect(doc.css(color_chip_selector).size).to be_zero
+ end
+
+ def code_tag(string)
+ "<code>#{string}</code>"
+ end
+end
diff --git a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
index 935146c17fc..a41a28a56f1 100644
--- a/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -53,7 +53,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do
doc = reference_filter("See (#{reference}.)")
exp = Regexp.escape(range.reference_link_text)
- expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{exp}</a>\.\)})
end
it 'ignores invalid commit IDs' do
@@ -222,7 +222,7 @@ describe Banzai::Filter::CommitRangeReferenceFilter do
doc = reference_filter("Fixed (#{reference}.)")
exp = Regexp.escape(range.reference_link_text(project))
- expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{exp}</a>\.\)})
end
it 'ignores invalid commit IDs on the referenced project' do
diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
index 080a5f57da9..35f8792ff35 100644
--- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -42,7 +42,7 @@ describe Banzai::Filter::CommitReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("See (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{commit.short_id}</a>\.\)})
end
it 'ignores invalid commit IDs' do
@@ -199,12 +199,12 @@ describe Banzai::Filter::CommitReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{commit.reference_link_text(project)}</a>\.\)})
end
it 'ignores invalid commit IDs on the referenced project' do
act = "Committed #{invalidate_reference(reference)}"
- expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
+ expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
end
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 a0d391d981c..d9018a7e4fe 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -49,7 +49,7 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do
it 'links with adjacent text' do
doc = filter("Issue (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{reference}</a>\.\)})
end
it 'includes a title attribute' do
diff --git a/spec/lib/banzai/filter/image_link_filter_spec.rb b/spec/lib/banzai/filter/image_link_filter_spec.rb
index 51920869545..c84b98eb225 100644
--- a/spec/lib/banzai/filter/image_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/image_link_filter_spec.rb
@@ -14,7 +14,7 @@ describe Banzai::Filter::ImageLinkFilter do
it 'does not wrap a duplicate link' do
doc = filter(%Q(<a href="/whatever">#{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')}</a>))
- expect(doc.to_html).to match /^<a href="\/whatever"><img[^>]*><\/a>$/
+ expect(doc.to_html).to match %r{^<a href="/whatever"><img[^>]*></a>$}
end
it 'works with external images' do
@@ -24,6 +24,6 @@ describe Banzai::Filter::ImageLinkFilter do
it 'works with inline images' do
doc = filter(%Q(<p>test #{image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg')} inline</p>))
- expect(doc.to_html).to match /^<p>test <a[^>]*><img[^>]*><\/a> inline<\/p>$/
+ expect(doc.to_html).to match %r{^<p>test <a[^>]*><img[^>]*></a> inline</p>$}
end
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 3a5f52ea23f..905fbb9434b 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -288,7 +288,7 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(issue.to_reference(project))} \(comment 123\)</a>\.\)})
end
it 'includes default classes' do
@@ -317,7 +317,7 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference_link}.)")
- expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>Reference</a>\.\)})
end
it 'includes default classes' do
@@ -346,7 +346,7 @@ describe Banzai::Filter::IssueReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Fixed (#{reference_link}.)")
- expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>Reference</a>\.\)})
end
it 'includes default classes' do
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 158844e25ae..eeb82822f68 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -42,7 +42,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Merge (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
end
it 'ignores invalid merge IDs' do
@@ -211,7 +211,7 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Merge (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(merge.to_reference(project))} \(diffs, comment 123\)</a>\.\)})
end
end
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
index 3a07a6dc179..e068e02d4fc 100644
--- a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -28,7 +28,7 @@ describe Banzai::Filter::SnippetReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Snippet (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(reference)}</a>\.\)})
end
it 'ignores invalid snippet IDs' do
@@ -192,13 +192,13 @@ describe Banzai::Filter::SnippetReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("See (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{Regexp.escape(snippet.to_reference(project))}</a>\.\)})
end
it 'ignores invalid snippet IDs on the referenced project' do
act = "See #{invalidate_reference(reference)}"
- expect(reference_filter(act).to_html).to match(/<a.+>#{Regexp.escape(invalidate_reference(reference))}<\/a>/)
+ expect(reference_filter(act).to_html).to match(%r{<a.+>#{Regexp.escape(invalidate_reference(reference))}</a>})
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index c76adc262fc..2f86a046d28 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -146,7 +146,7 @@ describe Banzai::Filter::UserReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Mention me (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>#{reference}</a>\.\)})
end
it 'includes default classes' do
@@ -172,7 +172,7 @@ describe Banzai::Filter::UserReferenceFilter do
it 'links with adjacent text' do
doc = reference_filter("Mention me (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/)
+ expect(doc.to_html).to match(%r{\(<a.+>User</a>\.\)})
end
it 'includes a data-user attribute' do
diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb
index c44bc1840df..ebd907ecb7f 100644
--- a/spec/lib/file_size_validator_spec.rb
+++ b/spec/lib/file_size_validator_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
describe FileSizeValidator do
let(:validator) { described_class.new(options) }
- let(:attachment) { AttachmentUploader.new }
let(:note) { create(:note) }
+ let(:attachment) { AttachmentUploader.new(note) }
describe 'options uses an integer' do
let(:options) { { maximum: 10, attributes: { attachment: attachment } } }
diff --git a/spec/lib/gitaly/server_spec.rb b/spec/lib/gitaly/server_spec.rb
new file mode 100644
index 00000000000..ed5d56e91d4
--- /dev/null
+++ b/spec/lib/gitaly/server_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Gitaly::Server do
+ describe '.all' do
+ let(:storages) { Gitlab.config.repositories.storages }
+
+ it 'includes all storages' do
+ expect(storages.count).to eq(described_class.all.count)
+ expect(storages.keys).to eq(described_class.all.map(&:storage))
+ end
+ end
+
+ subject { described_class.all.first }
+
+ it { is_expected.to respond_to(:server_version) }
+ it { is_expected.to respond_to(:git_binary_version) }
+ it { is_expected.to respond_to(:up_to_date?) }
+ it { is_expected.to respond_to(:address) }
+
+ describe 'request memoization' do
+ context 'when requesting multiple properties', :request_store do
+ it 'uses memoization for the info request' do
+ expect do
+ subject.server_version
+ subject.up_to_date?
+ end.to change { Gitlab::GitalyClient.get_request_count }.by(1)
+ end
+ end
+ end
+end
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 d21183b668b..c8df6dd2118 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
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :truncate, :migration, schema: 20171114162227 do
+describe Gitlab::BackgroundMigration::DeserializeMergeRequestDiffsAndCommits, :migration, schema: 20171114162227 do
let(:merge_request_diffs) { table(:merge_request_diffs) }
let(:merge_requests) { table(:merge_requests) }
diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
index 7b5a00c6111..021e1d14b18 100644
--- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do
+describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder, :delete do
let(:migration) { described_class.new }
before do
@@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do
end
describe '#perform' do
- it 'renames the path of system-uploads', :truncate do
+ it 'renames the path of system-uploads' do
upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg')
migration.perform('uploads/system/', 'uploads/-/system/')
diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
index 8bb9ebe0419..370c2490b97 100644
--- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
+++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb
@@ -23,6 +23,27 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
end
end
+ # E.g. The installation is in use at the time of migration, and someone has
+ # just uploaded a file
+ shared_examples 'does not add files in /uploads/tmp' do
+ let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
+
+ before do
+ FileUtils.mkdir(File.dirname(tmp_file))
+ FileUtils.touch(tmp_file)
+ end
+
+ after do
+ FileUtils.rm(tmp_file)
+ end
+
+ it 'does not add files from /uploads/tmp' do
+ described_class.new.perform
+
+ expect(untracked_files_for_uploads.count).to eq(5)
+ end
+ end
+
it 'ensures the untracked_files_for_uploads table exists' do
expect do
described_class.new.perform
@@ -109,24 +130,8 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
end
end
- # E.g. The installation is in use at the time of migration, and someone has
- # just uploaded a file
context 'when there are files in /uploads/tmp' do
- let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
-
- before do
- FileUtils.touch(tmp_file)
- end
-
- after do
- FileUtils.rm(tmp_file)
- end
-
- it 'does not add files from /uploads/tmp' do
- described_class.new.perform
-
- expect(untracked_files_for_uploads.count).to eq(5)
- end
+ it_behaves_like 'does not add files in /uploads/tmp'
end
end
end
@@ -197,24 +202,8 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
end
end
- # E.g. The installation is in use at the time of migration, and someone has
- # just uploaded a file
context 'when there are files in /uploads/tmp' do
- let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
-
- before do
- FileUtils.touch(tmp_file)
- end
-
- after do
- FileUtils.rm(tmp_file)
- end
-
- it 'does not add files from /uploads/tmp' do
- described_class.new.perform
-
- expect(untracked_files_for_uploads.count).to eq(5)
- end
+ it_behaves_like 'does not add files in /uploads/tmp'
end
end
end
diff --git a/spec/lib/gitlab/badge/coverage/template_spec.rb b/spec/lib/gitlab/badge/coverage/template_spec.rb
index 383bae6e087..d9c21a22590 100644
--- a/spec/lib/gitlab/badge/coverage/template_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/template_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::Badge::Coverage::Template do
- let(:badge) { double(entity: 'coverage', status: 90) }
+ let(:badge) { double(entity: 'coverage', status: 90.00) }
let(:template) { described_class.new(badge) }
describe '#key_text' do
@@ -13,7 +13,17 @@ describe Gitlab::Badge::Coverage::Template do
describe '#value_text' do
context 'when coverage is known' do
it 'returns coverage percentage' do
- expect(template.value_text).to eq '90%'
+ expect(template.value_text).to eq '90.00%'
+ end
+ end
+
+ context 'when coverage is known to many digits' do
+ before do
+ allow(badge).to receive(:status).and_return(92.349)
+ end
+
+ it 'returns rounded coverage percentage' do
+ expect(template.value_text).to eq '92.35%'
end
end
@@ -37,7 +47,7 @@ describe Gitlab::Badge::Coverage::Template do
describe '#value_width' do
context 'when coverage is known' do
it 'is narrower when coverage is known' do
- expect(template.value_width).to eq 36
+ expect(template.value_width).to eq 54
end
end
@@ -113,7 +123,7 @@ describe Gitlab::Badge::Coverage::Template do
describe '#width' do
context 'when coverage is known' do
it 'returns the key width plus value width' do
- expect(template.width).to eq 98
+ expect(template.width).to eq 116
end
end
diff --git a/spec/lib/gitlab/current_settings_spec.rb b/spec/lib/gitlab/current_settings_spec.rb
index 492659a82b0..4ddcbd7eb66 100644
--- a/spec/lib/gitlab/current_settings_spec.rb
+++ b/spec/lib/gitlab/current_settings_spec.rb
@@ -8,22 +8,37 @@ describe Gitlab::CurrentSettings do
end
describe '#current_application_settings' do
+ it 'allows keys to be called directly' do
+ db_settings = create(:application_setting,
+ home_page_url: 'http://mydomain.com',
+ signup_enabled: false)
+
+ expect(described_class.home_page_url).to eq(db_settings.home_page_url)
+ expect(described_class.signup_enabled?).to be_falsey
+ expect(described_class.signup_enabled).to be_falsey
+ expect(described_class.metrics_sample_interval).to be(15)
+ end
+
context 'with DB available' do
before do
- allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(true)
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(true)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(true)
+ allow(ActiveRecord::Base.connection).to receive(:table_exists?).and_call_original
+ allow(ActiveRecord::Base.connection).to receive(:table_exists?).with('application_settings').and_return(true)
end
it 'attempts to use cached values first' do
expect(ApplicationSetting).to receive(:cached)
- expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(described_class.current_application_settings).to be_a(ApplicationSetting)
end
it 'falls back to DB if Redis returns an empty value' do
expect(ApplicationSetting).to receive(:cached).and_return(nil)
expect(ApplicationSetting).to receive(:last).and_call_original.twice
- expect(current_application_settings).to be_a(ApplicationSetting)
+ expect(described_class.current_application_settings).to be_a(ApplicationSetting)
end
it 'falls back to DB if Redis fails' do
@@ -32,14 +47,14 @@ describe Gitlab::CurrentSettings do
expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError)
expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError)
- expect(current_application_settings).to eq(db_settings)
+ expect(described_class.current_application_settings).to eq(db_settings)
end
it 'creates default ApplicationSettings if none are present' do
expect(ApplicationSetting).to receive(:cached).and_raise(::Redis::BaseError)
expect(Rails.cache).to receive(:fetch).with(ApplicationSetting::CACHE_KEY).and_raise(Redis::BaseError)
- settings = current_application_settings
+ settings = described_class.current_application_settings
expect(settings).to be_a(ApplicationSetting)
expect(settings).to be_persisted
@@ -52,7 +67,7 @@ describe Gitlab::CurrentSettings do
end
it 'returns an in-memory ApplicationSetting object' do
- settings = current_application_settings
+ settings = described_class.current_application_settings
expect(settings).to be_a(OpenStruct)
expect(settings.sign_in_enabled?).to eq(settings.sign_in_enabled)
@@ -63,7 +78,7 @@ describe Gitlab::CurrentSettings do
db_settings = create(:application_setting,
home_page_url: 'http://mydomain.com',
signup_enabled: false)
- settings = current_application_settings
+ settings = described_class.current_application_settings
app_defaults = ApplicationSetting.last
expect(settings).to be_a(OpenStruct)
@@ -80,15 +95,16 @@ describe Gitlab::CurrentSettings do
context 'with DB unavailable' do
before do
- allow_any_instance_of(described_class).to receive(:connect_to_db?).and_return(false)
- allow_any_instance_of(described_class).to receive(:retrieve_settings_from_database_cache?).and_return(nil)
+ # For some reason, `allow(described_class).to receive(:connect_to_db?).and_return(false)` causes issues
+ # during the initialization phase of the test suite, so instead let's mock the internals of it
+ allow(ActiveRecord::Base.connection).to receive(:active?).and_return(false)
end
it 'returns an in-memory ApplicationSetting object' do
expect(ApplicationSetting).not_to receive(:current)
expect(ApplicationSetting).not_to receive(:last)
- expect(current_application_settings).to be_a(OpenStruct)
+ expect(described_class.current_application_settings).to be_a(OpenStruct)
end
end
@@ -101,8 +117,8 @@ describe Gitlab::CurrentSettings do
expect(ApplicationSetting).not_to receive(:current)
expect(ApplicationSetting).not_to receive(:last)
- expect(current_application_settings).to be_a(ApplicationSetting)
- expect(current_application_settings).not_to be_persisted
+ expect(described_class.current_application_settings).to be_a(ApplicationSetting)
+ expect(described_class.current_application_settings).not_to be_persisted
end
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 596cc435bd9..cc7cb3f23fd 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
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :truncate do
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :delete do
let(:migration) { FakeRenameReservedPathMigrationV1.new }
let(:subject) { described_class.new(['the-path'], migration) }
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
index 1143182531f..f31475dbd71 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :truncate do
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, :delete do
let(:migration) { FakeRenameReservedPathMigrationV1.new }
let(:subject) { described_class.new(['the-path'], migration) }
let(:namespace) { create(:group, name: 'the-path') }
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
index e850b5cd6a4..0958144643b 100644
--- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :truncate do
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :delete do
let(:migration) { FakeRenameReservedPathMigrationV1.new }
let(:subject) { described_class.new(['the-path'], migration) }
let(:project) do
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 7695b95dc57..1d31f96159c 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
@@ -13,7 +13,7 @@ shared_examples 'renames child namespaces' do |type|
end
end
-describe Gitlab::Database::RenameReservedPathsMigration::V1, :truncate do
+describe Gitlab::Database::RenameReservedPathsMigration::V1, :delete do
let(:subject) { FakeRenameReservedPathMigrationV1.new }
before do
diff --git a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
index 39e3b875c49..326ed2f2ecf 100644
--- a/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/uploads_rewriter_spec.rb
@@ -17,7 +17,7 @@ describe Gitlab::Gfm::UploadsRewriter do
end
let(:text) do
- "Text and #{image_uploader.to_markdown} and #{zip_uploader.to_markdown}"
+ "Text and #{image_uploader.markdown_link} and #{zip_uploader.markdown_link}"
end
describe '#rewrite' do
diff --git a/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
new file mode 100644
index 00000000000..5d22dcfb508
--- /dev/null
+++ b/spec/lib/gitlab/git/attributes_at_ref_parser_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Gitlab::Git::AttributesAtRefParser, seed_helper: true do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+
+ subject { described_class.new(repository, 'lfs') }
+
+ it 'loads .gitattributes blob' do
+ repository.raw # Initialize repository in advance since this also checks attributes
+
+ expected_filter = 'filter=lfs diff=lfs merge=lfs'
+ receive_blob = receive(:new).with(a_string_including(expected_filter))
+ expect(Gitlab::Git::AttributesParser).to receive_blob.and_call_original
+
+ subject
+ end
+
+ it 'handles missing blobs' do
+ expect { described_class.new(repository, 'non-existant-branch') }.not_to raise_error
+ end
+
+ describe '#attributes' do
+ it 'returns the attributes as a Hash' do
+ expect(subject.attributes('test.lfs')['filter']).to eq('lfs')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/attributes_spec.rb b/spec/lib/gitlab/git/attributes_parser_spec.rb
index b715fc3410a..323334e99a5 100644
--- a/spec/lib/gitlab/git/attributes_spec.rb
+++ b/spec/lib/gitlab/git/attributes_parser_spec.rb
@@ -1,11 +1,10 @@
require 'spec_helper'
-describe Gitlab::Git::Attributes, seed_helper: true do
- let(:path) do
- File.join(SEED_STORAGE_PATH, 'with-git-attributes.git')
- end
+describe Gitlab::Git::AttributesParser, seed_helper: true do
+ let(:attributes_path) { File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info', 'attributes') }
+ let(:data) { File.read(attributes_path) }
- subject { described_class.new(path) }
+ subject { described_class.new(data) }
describe '#attributes' do
context 'using a path with attributes' do
@@ -66,6 +65,26 @@ describe Gitlab::Git::Attributes, seed_helper: true do
expect(subject.attributes('test.foo')).to eq({})
end
end
+
+ context 'when attributes data is a file handle' do
+ subject do
+ File.open(attributes_path, 'r') do |file_handle|
+ described_class.new(file_handle)
+ end
+ end
+
+ it 'returns the attributes as a Hash' do
+ expect(subject.attributes('test.txt')).to eq({ 'text' => true })
+ end
+ end
+
+ context 'when attributes data is nil' do
+ let(:data) { nil }
+
+ it 'returns an empty Hash' do
+ expect(subject.attributes('test.foo')).to eq({})
+ end
+ end
end
describe '#patterns' do
@@ -74,14 +93,14 @@ describe Gitlab::Git::Attributes, seed_helper: true do
end
it 'parses an entry that uses a tab to separate the pattern and attributes' do
- expect(subject.patterns[File.join(path, '*.md')])
+ expect(subject.patterns[File.join('/', '*.md')])
.to eq({ 'gitlab-language' => 'markdown' })
end
it 'stores patterns in reverse order' do
first = subject.patterns.to_a[0]
- expect(first[0]).to eq(File.join(path, 'bla/bla.txt'))
+ expect(first[0]).to eq(File.join('/', 'bla/bla.txt'))
end
# It's a bit hard to test for something _not_ being processed. As such we'll
@@ -89,14 +108,6 @@ describe Gitlab::Git::Attributes, seed_helper: true do
it 'ignores any comments and empty lines' do
expect(subject.patterns.length).to eq(10)
end
-
- it 'does not parse anything when the attributes file does not exist' do
- expect(File).to receive(:exist?)
- .with(File.join(path, 'info/attributes'))
- .and_return(false)
-
- expect(subject.patterns).to eq({})
- end
end
describe '#parse_attributes' do
@@ -132,17 +143,9 @@ describe Gitlab::Git::Attributes, seed_helper: true do
expect { |b| subject.each_line(&b) }.to yield_successive_args(*args)
end
- it 'does not yield when the attributes file does not exist' do
- expect(File).to receive(:exist?)
- .with(File.join(path, 'info/attributes'))
- .and_return(false)
-
- expect { |b| subject.each_line(&b) }.not_to yield_control
- end
-
it 'does not yield when the attributes file has an unsupported encoding' do
- path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git')
- attrs = described_class.new(path)
+ path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info', 'attributes')
+ attrs = described_class.new(File.read(path))
expect { |b| attrs.each_line(&b) }.not_to yield_control
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index 8706c89c147..8ac960133c5 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -260,29 +260,57 @@ describe Gitlab::Git::Blob, seed_helper: true do
)
end
- it 'returns a list of Gitlab::Git::Blob' do
- blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
+ shared_examples 'fetching batch of LFS pointers' do
+ it 'returns a list of Gitlab::Git::Blob' do
+ blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id])
- expect(blobs.count).to eq(1)
- expect(blobs).to all( be_a(Gitlab::Git::Blob) )
- end
+ expect(blobs.count).to eq(1)
+ expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ end
- it 'silently ignores tree objects' do
- blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
+ it 'accepts blob IDs as a lazy enumerator' do
+ blobs = described_class.batch_lfs_pointers(repository, [lfs_blob.id].lazy)
- expect(blobs).to eq([])
- end
+ expect(blobs.count).to eq(1)
+ expect(blobs).to all( be_a(Gitlab::Git::Blob) )
+ end
+
+ it 'handles empty list of IDs gracefully' do
+ blobs_1 = described_class.batch_lfs_pointers(repository, [].lazy)
+ blobs_2 = described_class.batch_lfs_pointers(repository, [])
+
+ expect(blobs_1).to eq([])
+ expect(blobs_2).to eq([])
+ end
+
+ it 'silently ignores tree objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [tree_object.oid])
- it 'silently ignores non lfs objects' do
- blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+ expect(blobs).to eq([])
+ end
+
+ it 'silently ignores non lfs objects' do
+ blobs = described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+
+ expect(blobs).to eq([])
+ end
- expect(blobs).to eq([])
+ it 'avoids loading large blobs into memory' do
+ # This line could call `lookup` on `repository`, so do here before mocking.
+ non_lfs_blob_id = non_lfs_blob.id
+
+ expect(repository).not_to receive(:lookup)
+
+ described_class.batch_lfs_pointers(repository, [non_lfs_blob_id])
+ end
end
- it 'avoids loading large blobs into memory' do
- expect(repository).not_to receive(:lookup)
+ context 'when Gitaly batch_lfs_pointers is enabled' do
+ it_behaves_like 'fetching batch of LFS pointers'
+ end
- described_class.batch_lfs_pointers(repository, [non_lfs_blob.id])
+ context 'when Gitaly batch_lfs_pointers is disabled', :disable_gitaly do
+ it_behaves_like 'fetching batch of LFS pointers'
end
end
diff --git a/spec/lib/gitlab/git/info_attributes_spec.rb b/spec/lib/gitlab/git/info_attributes_spec.rb
new file mode 100644
index 00000000000..ea84909c3e0
--- /dev/null
+++ b/spec/lib/gitlab/git/info_attributes_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Gitlab::Git::InfoAttributes, seed_helper: true do
+ let(:path) do
+ File.join(SEED_STORAGE_PATH, 'with-git-attributes.git')
+ end
+
+ subject { described_class.new(path) }
+
+ describe '#attributes' do
+ context 'using a path with attributes' do
+ it 'returns the attributes as a Hash' do
+ expect(subject.attributes('test.txt')).to eq({ 'text' => true })
+ end
+
+ it 'returns an empty Hash for a defined path without attributes' do
+ expect(subject.attributes('bla/bla.txt')).to eq({})
+ end
+ end
+ end
+
+ describe '#parser' do
+ it 'parses a file with entries' do
+ expect(subject.patterns).to be_an_instance_of(Hash)
+ expect(subject.patterns["/*.txt"]).to eq({ 'text' => true })
+ end
+
+ it 'does not parse anything when the attributes file does not exist' do
+ expect(File).to receive(:exist?)
+ .with(File.join(path, 'info/attributes'))
+ .and_return(false)
+
+ expect(subject.patterns).to eq({})
+ end
+
+ it 'does not parse attributes files with unsupported encoding' do
+ path = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git')
+ subject = described_class.new(path)
+
+ expect(subject.patterns).to eq({})
+ end
+ end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index aec7cde6df8..ec1c7a96f92 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -2,6 +2,7 @@ require "spec_helper"
describe Gitlab::Git::Repository, seed_helper: true do
include Gitlab::EncodingHelper
+ using RSpec::Parameterized::TableSyntax
shared_examples 'wrapping gRPC errors' do |gitaly_client_class, gitaly_client_method|
it 'wraps gRPC not found error' do
@@ -19,6 +20,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') }
let(:storage_path) { TestEnv.repos_path }
+ let(:user) { build(:user) }
describe '.create_hooks' do
let(:repo_path) { File.join(storage_path, 'hook-test.git') }
@@ -248,7 +250,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
shared_examples 'archive check' do |extenstion|
- it { expect(metadata['ArchivePath']).to match(/tmp\/gitlab-git-test.git\/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}/) }
+ it { expect(metadata['ArchivePath']).to match(%r{tmp/gitlab-git-test.git/gitlab-git-test-master-#{SeedRepo::LastCommit::ID}}) }
it { expect(metadata['ArchivePath']).to end_with extenstion }
end
@@ -442,6 +444,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples 'simple commit counting' do
it { expect(repository.commit_count("master")).to eq(25) }
it { expect(repository.commit_count("feature")).to eq(9) }
+ it { expect(repository.commit_count("does-not-exist")).to eq(0) }
end
context 'when Gitaly commit_count feature is enabled' do
@@ -560,35 +563,39 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#delete_refs' do
- before(:all) do
- @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
- end
+ shared_examples 'deleting refs' do
+ let(:repo) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') }
- it 'deletes the ref' do
- @repo.delete_refs('refs/heads/feature')
+ after do
+ ensure_seeds
+ end
- expect(@repo.rugged.references['refs/heads/feature']).to be_nil
- end
+ it 'deletes the ref' do
+ repo.delete_refs('refs/heads/feature')
- it 'deletes all refs' do
- refs = %w[refs/heads/wip refs/tags/v1.1.0]
- @repo.delete_refs(*refs)
+ expect(repo.rugged.references['refs/heads/feature']).to be_nil
+ end
+
+ it 'deletes all refs' do
+ refs = %w[refs/heads/wip refs/tags/v1.1.0]
+ repo.delete_refs(*refs)
- refs.each do |ref|
- expect(@repo.rugged.references[ref]).to be_nil
+ refs.each do |ref|
+ expect(repo.rugged.references[ref]).to be_nil
+ end
end
- end
- it 'raises an error if it failed' do
- expect(@repo).to receive(:popen).and_return(['Error', 1])
+ it 'raises an error if it failed' do
+ expect { repo.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError)
+ end
+ end
- expect do
- @repo.delete_refs('refs/heads/fix')
- end.to raise_error(Gitlab::Git::Repository::GitError)
+ context 'when Gitaly delete_refs feature is enabled' do
+ it_behaves_like 'deleting refs'
end
- after(:all) do
- ensure_seeds
+ context 'when Gitaly delete_refs feature is disabled', :disable_gitaly do
+ it_behaves_like 'deleting refs'
end
end
@@ -687,7 +694,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
describe '#remote_tags' do
let(:remote_name) { 'upstream' }
let(:target_commit_id) { SeedRepo::Commit::ID }
- let(:user) { create(:user) }
let(:tag_name) { 'v0.0.1' }
let(:tag_message) { 'My tag' }
let(:remote_repository) do
@@ -943,6 +949,16 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
end
+
+ context 'limit validation' do
+ where(:limit) do
+ [0, nil, '', 'foo']
+ end
+
+ with_them do
+ it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) }
+ end
+ end
end
describe "#rugged_commits_between" do
@@ -984,6 +1000,29 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { is_expected.to eq(17) }
end
+ describe '#merge_base' do
+ shared_examples '#merge_base' do
+ where(:from, :to, :result) do
+ '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d'
+ '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d'
+ '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil
+ 'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil
+ end
+
+ with_them do
+ it { expect(repository.merge_base(from, to)).to eq(result) }
+ end
+ end
+
+ context 'with gitaly' do
+ it_behaves_like '#merge_base'
+ end
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#merge_base'
+ end
+ end
+
describe '#count_commits' do
shared_examples 'extended commit counting' do
context 'with after timestamp' do
@@ -1123,14 +1162,27 @@ describe Gitlab::Git::Repository, seed_helper: true do
context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do
it_behaves_like 'finding a branch'
- it 'should reload Rugged::Repository and return master' do
- expect(Rugged::Repository).to receive(:new).twice.and_call_original
+ context 'force_reload is true' do
+ it 'should reload Rugged::Repository' do
+ expect(Rugged::Repository).to receive(:new).twice.and_call_original
- repository.find_branch('master')
- branch = repository.find_branch('master', force_reload: true)
+ repository.find_branch('master')
+ branch = repository.find_branch('master', force_reload: true)
- expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
- expect(branch.name).to eq('master')
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
+ end
+
+ context 'force_reload is false' do
+ it 'should not reload Rugged::Repository' do
+ expect(Rugged::Repository).to receive(:new).once.and_call_original
+
+ branch = repository.find_branch('master', force_reload: false)
+
+ expect(branch).to be_a_kind_of(Gitlab::Git::Branch)
+ expect(branch.name).to eq('master')
+ end
end
end
end
@@ -1672,7 +1724,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
shared_examples "user deleting a branch" do
let(:project) { create(:project, :repository) }
let(:repository) { project.repository.raw }
- let(:user) { create(:user) }
let(:branch_name) { "to-be-deleted-soon" }
before do
@@ -1713,12 +1764,49 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#write_config' do
+ before do
+ repository.rugged.config["gitlab.fullpath"] = repository.path
+ end
+
+ shared_examples 'writing repo config' do
+ context 'is given a path' do
+ it 'writes it to disk' do
+ repository.write_config(full_path: "not-the/real-path.git")
+
+ config = File.read(File.join(repository.path, "config"))
+
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = not-the/real-path.git")
+ end
+ end
+
+ context 'it is given an empty path' do
+ it 'does not write it to disk' do
+ repository.write_config(full_path: "")
+
+ config = File.read(File.join(repository.path, "config"))
+
+ expect(config).to include("[gitlab]")
+ expect(config).to include("fullpath = #{repository.path}")
+ end
+ end
+ end
+
+ context "when gitaly_write_config is enabled" do
+ it_behaves_like "writing repo config"
+ end
+
+ context "when gitaly_write_config is disabled", :disable_gitaly do
+ it_behaves_like "writing repo config"
+ end
+ end
+
describe '#merge' do
let(:repository) do
Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
end
let(:source_sha) { '913c66a37b4a45b9769037c55c2d238bd0942d2e' }
- let(:user) { build(:user) }
let(:target_branch) { 'test-merge-target-branch' }
before do
@@ -1771,7 +1859,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
let(:branch_head) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' }
- let(:user) { build(:user) }
let(:target_branch) { 'test-ff-target-branch' }
before do
@@ -1926,6 +2013,75 @@ describe Gitlab::Git::Repository, seed_helper: true do
it { expect(subject.repository_relative_path).to eq(repository.relative_path) }
end
+ describe '#bundle_to_disk' do
+ shared_examples 'bundling to disk' do
+ let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
+
+ after do
+ FileUtils.rm_rf(save_path)
+ end
+
+ it 'saves a bundle to disk' do
+ repository.bundle_to_disk(save_path)
+
+ success = system(
+ *%W(#{Gitlab.config.git.bin_path} -C #{repository.path} bundle verify #{save_path}),
+ [:out, :err] => '/dev/null'
+ )
+ expect(success).to be true
+ end
+ end
+
+ context 'when Gitaly bundle_to_disk feature is enabled' do
+ it_behaves_like 'bundling to disk'
+ end
+
+ context 'when Gitaly bundle_to_disk feature is disabled', :disable_gitaly do
+ it_behaves_like 'bundling to disk'
+ end
+ end
+
+ describe '#create_from_bundle' do
+ shared_examples 'creating repo from bundle' do
+ let(:bundle_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") }
+ let(:project) { create(:project) }
+ let(:imported_repo) { project.repository.raw }
+
+ before do
+ expect(repository.bundle_to_disk(bundle_path)).to be true
+ end
+
+ after do
+ FileUtils.rm_rf(bundle_path)
+ end
+
+ it 'creates a repo from a bundle file' do
+ expect(imported_repo).not_to exist
+
+ result = imported_repo.create_from_bundle(bundle_path)
+
+ expect(result).to be true
+ expect(imported_repo).to exist
+ expect { imported_repo.fsck }.not_to raise_exception
+ end
+
+ it 'creates a symlink to the global hooks dir' do
+ imported_repo.create_from_bundle(bundle_path)
+ hooks_path = File.join(imported_repo.path, 'hooks')
+
+ expect(File.readlink(hooks_path)).to eq(Gitlab.config.gitlab_shell.hooks_path)
+ end
+ end
+
+ context 'when Gitaly create_repo_from_bundle feature is enabled' do
+ it_behaves_like 'creating repo from bundle'
+ end
+
+ context 'when Gitaly create_repo_from_bundle feature is disabled', :disable_gitaly do
+ it_behaves_like 'creating repo from bundle'
+ end
+ end
+
context 'gitlab_projects commands' do
let(:gitlab_projects) { repository.gitlab_projects }
let(:timeout) { Gitlab.config.gitlab_shell.git_timeout }
@@ -2021,6 +2177,47 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error')
end
end
+
+ describe '#squash' do
+ let(:squash_id) { '1' }
+ let(:branch_name) { 'fix' }
+ let(:start_sha) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
+ let(:end_sha) { '12d65c8dd2b2676fa3ac47d955accc085a37a9c1' }
+
+ subject do
+ opts = {
+ branch: branch_name,
+ start_sha: start_sha,
+ end_sha: end_sha,
+ author: user,
+ message: 'Squash commit message'
+ }
+
+ repository.squash(user, squash_id, opts)
+ end
+
+ context 'sparse checkout', :skip_gitaly_mock do
+ let(:expected_files) { %w(files files/js files/js/application.js) }
+
+ before do
+ allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args|
+ m.call(*args) do
+ worktree_path = args[0]
+ files_pattern = File.join(worktree_path, '**', '*')
+ expected = expected_files.map do |path|
+ File.expand_path(path, worktree_path)
+ end
+
+ expect(Dir[files_pattern]).to eq(expected)
+ end
+ end
+ end
+
+ it 'checkouts only the files in the diff' do
+ subject
+ end
+ end
+ end
end
def create_remote_branch(repository, remote_name, branch_name, source_branch_name)
diff --git a/spec/lib/gitlab/git/tree_spec.rb b/spec/lib/gitlab/git/tree_spec.rb
index 86f7bcb8e38..001e406a930 100644
--- a/spec/lib/gitlab/git/tree_spec.rb
+++ b/spec/lib/gitlab/git/tree_spec.rb
@@ -80,22 +80,18 @@ describe Gitlab::Git::Tree, seed_helper: true do
end
describe '#where' do
- context 'with gitaly disabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
- end
-
- it 'calls #tree_entries_from_rugged' do
- expect(described_class).to receive(:tree_entries_from_rugged)
-
- described_class.where(repository, SeedRepo::Commit::ID, '/')
+ shared_examples '#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([])
end
end
- it 'gets the tree entries from GitalyClient' do
- expect_any_instance_of(Gitlab::GitalyClient::CommitService).to receive(:tree_entries)
+ context 'with gitaly' do
+ it_behaves_like '#where'
+ end
- described_class.where(repository, SeedRepo::Commit::ID, '/')
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like '#where'
end
end
end
diff --git a/spec/lib/gitlab/git/wiki_spec.rb b/spec/lib/gitlab/git/wiki_spec.rb
new file mode 100644
index 00000000000..bd8dbf07fa7
--- /dev/null
+++ b/spec/lib/gitlab/git/wiki_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Wiki do
+ let(:project) { create(:project) }
+ let(:user) { project.owner }
+ let(:wiki) { ProjectWiki.new(project, user) }
+ let(:gollum_wiki) { wiki.wiki }
+
+ # Remove skip_gitaly_mock flag when gitaly_find_page when
+ # https://gitlab.com/gitlab-org/gitaly/merge_requests/539 gets merged
+ describe '#page', :skip_gitaly_mock do
+ it 'returns the right page' do
+ create_page('page1', 'content')
+ create_page('foo/page1', 'content')
+
+ expect(gollum_wiki.page(title: 'page1', dir: '').url_path).to eq 'page1'
+ expect(gollum_wiki.page(title: 'page1', dir: 'foo').url_path).to eq 'foo/page1'
+
+ destroy_page('page1')
+ destroy_page('page1', 'foo')
+ end
+ end
+
+ def create_page(name, content)
+ gollum_wiki.write_page(name, :markdown, content, commit_details)
+ end
+
+ def commit_details
+ Gitlab::Git::Wiki::CommitDetails.new(user.name, user.email, "test commit")
+ end
+
+ def destroy_page(title, dir = '')
+ page = gollum_wiki.page(title: title, dir: dir)
+ wiki.delete_page(page, "test commit")
+ 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 b2275119a04..3722a91c050 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -131,6 +131,29 @@ describe Gitlab::GitalyClient::CommitService do
end
end
+ describe '#commit_count' do
+ before do
+ expect_any_instance_of(Gitaly::CommitService::Stub)
+ .to receive(:count_commits)
+ .with(gitaly_request_with_path(storage_name, relative_path),
+ kind_of(Hash))
+ .and_return([])
+ end
+
+ it 'sends a commit_count message' do
+ client.commit_count(revision)
+ end
+
+ context 'with UTF-8 params strings' do
+ let(:revision) { "branch\u011F" }
+ let(:path) { "foo/\u011F.txt" }
+
+ it 'handles string encodings correctly' do
+ client.commit_count(revision, path: path)
+ end
+ end
+ end
+
describe '#find_commit' do
let(:revision) { '4b825dc642cb6eb9a060e54bf8d69288fbee4904' }
it 'sends an RPC request' do
diff --git a/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb
new file mode 100644
index 00000000000..2c7e5eb5787
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/health_check_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::HealthCheckService do
+ let(:project) { create(:project) }
+ let(:storage_name) { project.repository_storage }
+
+ subject { described_class.new(storage_name) }
+
+ describe '#check' do
+ it 'successfully sends a health check request' do
+ expect(Gitlab::GitalyClient).to receive(:call).with(
+ storage_name,
+ :health_check,
+ :check,
+ instance_of(Grpc::Health::V1::HealthCheckRequest),
+ timeout: Gitlab::GitalyClient.fast_timeout).and_call_original
+
+ expect(subject.check).to eq({ success: true })
+ end
+
+ it 'receives an unsuccessful health check request' do
+ expect_any_instance_of(Grpc::Health::V1::Health::Stub)
+ .to receive(:check)
+ .and_return(double(status: false))
+
+ expect(subject.check).to eq({ success: false })
+ end
+
+ it 'gracefully handles gRPC error' do
+ expect(Gitlab::GitalyClient).to receive(:call).with(
+ storage_name,
+ :health_check,
+ :check,
+ instance_of(Grpc::Health::V1::HealthCheckRequest),
+ timeout: Gitlab::GitalyClient.fast_timeout)
+ .and_raise(GRPC::Unavailable.new('Connection refused'))
+
+ expect(subject.check).to eq({ success: false, message: '14:Connection refused' })
+ 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 d9ec28ab02e..9fbdd73ee0e 100644
--- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb
@@ -123,4 +123,53 @@ describe Gitlab::GitalyClient::OperationService do
expect(subject.branch_created).to be(false)
end
end
+
+ describe '#user_squash' do
+ let(:branch_name) { 'my-branch' }
+ let(:squash_id) { '1' }
+ let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' }
+ let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' }
+ let(:commit_message) { 'Squash message' }
+ let(:request) do
+ Gitaly::UserSquashRequest.new(
+ repository: repository.gitaly_repository,
+ user: gitaly_user,
+ squash_id: squash_id.to_s,
+ branch: branch_name,
+ start_sha: start_sha,
+ end_sha: end_sha,
+ author: gitaly_user,
+ commit_message: commit_message
+ )
+ end
+ let(:squash_sha) { 'f00' }
+ let(:response) { Gitaly::UserSquashResponse.new(squash_sha: squash_sha) }
+
+ subject do
+ client.user_squash(user, squash_id, branch_name, start_sha, end_sha, user, commit_message)
+ end
+
+ it 'sends a user_squash message and returns the squash sha' do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_squash).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect(subject).to eq(squash_sha)
+ end
+
+ context "when git_error is present" do
+ let(:response) do
+ Gitaly::UserSquashResponse.new(git_error: "something failed")
+ end
+
+ it "throws a PreReceive exception" do
+ expect_any_instance_of(Gitaly::OperationService::Stub)
+ .to receive(:user_squash).with(request, kind_of(Hash))
+ .and_return(response)
+
+ expect { subject }.to raise_error(
+ Gitlab::Git::Repository::GitError, "something failed")
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 951e146a30a..257e4c50f2d 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -112,7 +112,7 @@ describe Gitlab::GitalyClient::RefService do
expect_any_instance_of(Gitaly::RefService::Stub)
.to receive(:delete_refs)
.with(gitaly_request_with_params(except_with_prefix: prefixes), kind_of(Hash))
- .and_return(double('delete_refs_response'))
+ .and_return(double('delete_refs_response', git_error: ""))
client.delete_refs(except_with_prefixes: prefixes)
end
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 309b7338ef0..81bcd8c28ed 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -3,6 +3,31 @@ require 'spec_helper'
# We stub Gitaly in `spec/support/gitaly.rb` for other tests. We don't want
# those stubs while testing the GitalyClient itself.
describe Gitlab::GitalyClient, skip_gitaly_mock: true do
+ describe '.stub_class' do
+ it 'returns the gRPC health check stub' do
+ expect(described_class.stub_class(:health_check)).to eq(::Grpc::Health::V1::Health::Stub)
+ end
+
+ it 'returns a Gitaly stub' do
+ expect(described_class.stub_class(:ref_service)).to eq(::Gitaly::RefService::Stub)
+ end
+ end
+
+ describe '.stub_address' do
+ it 'returns the same result after being called multiple times' do
+ address = 'localhost:9876'
+ prefixed_address = "tcp://#{address}"
+
+ allow(Gitlab.config.repositories).to receive(:storages).and_return({
+ 'default' => { 'gitaly_address' => prefixed_address }
+ })
+
+ 2.times do
+ expect(described_class.stub_address('default')).to eq('localhost:9876')
+ end
+ end
+ end
+
describe '.stub' do
# Notice that this is referring to gRPC "stubs", not rspec stubs
before do
diff --git a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
index d72572cd510..44695acbe7d 100644
--- a/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_requests_importer_spec.rb
@@ -244,7 +244,7 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
it 'returns true when a commit exists' do
expect(project.repository)
- .to receive(:lookup)
+ .to receive(:commit)
.with('123')
.and_return(double(:commit))
@@ -253,9 +253,9 @@ describe Gitlab::GithubImport::Importer::PullRequestsImporter do
it 'returns false when a commit does not exist' do
expect(project.repository)
- .to receive(:lookup)
+ .to receive(:commit)
.with('123')
- .and_raise(Rugged::OdbError)
+ .and_return(nil)
expect(importer.commit_exists?('123')).to eq(false)
end
diff --git a/spec/lib/gitlab/health_checks/gitaly_check_spec.rb b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
new file mode 100644
index 00000000000..724beefff69
--- /dev/null
+++ b/spec/lib/gitlab/health_checks/gitaly_check_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Gitlab::HealthChecks::GitalyCheck do
+ let(:result_class) { Gitlab::HealthChecks::Result }
+ let(:repository_storages) { ['default'] }
+
+ before do
+ allow(described_class).to receive(:repository_storages) { repository_storages }
+ end
+
+ describe '#readiness' do
+ subject { described_class.readiness }
+
+ before do
+ expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check)
+ end
+
+ context 'Gitaly server is up' do
+ let(:gitaly_check) { double(check: { success: true }) }
+
+ it { is_expected.to eq([result_class.new(true, nil, shard: 'default')]) }
+ end
+
+ context 'Gitaly server is down' do
+ let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) }
+
+ it { is_expected.to eq([result_class.new(false, 'Connection refused', shard: 'default')]) }
+ end
+ end
+
+ describe '#metrics' do
+ subject { described_class.metrics }
+
+ before do
+ expect(Gitlab::GitalyClient::HealthCheckService).to receive(:new).and_return(gitaly_check)
+ end
+
+ context 'Gitaly server is up' do
+ let(:gitaly_check) { double(check: { success: true }) }
+
+ it 'provides metrics' do
+ expect(subject).to all(have_attributes(labels: { shard: 'default' }))
+ expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 1))
+ expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0))
+ end
+ end
+
+ context 'Gitaly server is down' do
+ let(:gitaly_check) { double(check: { success: false, message: 'Connection refused' }) }
+
+ it 'provides metrics' do
+ expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_success', value: 0))
+ expect(subject).to include(an_object_having_attributes(name: 'gitaly_health_check_latency_seconds', value: be >= 0))
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 4cf33778d15..b6c1f0c81cb 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -7096,7 +7096,7 @@
"project_id": 5,
"created_at": "2016-06-14T15:01:51.232Z",
"updated_at": "2016-06-14T15:01:51.232Z",
- "active": false,
+ "active": true,
"properties": {
},
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 9dfd879a1bc..d076007e4bc 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -236,12 +236,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
labels = project.issues.first.labels
expect(labels.where(type: "ProjectLabel").count).to eq(results.fetch(:first_issue_labels, 0))
+ expect(labels.where(type: "ProjectLabel").where.not(group_id: nil).count).to eq(0)
end
end
shared_examples 'restores group correctly' do |**results|
it 'has group label' do
expect(project.group.labels.size).to eq(results.fetch(:labels, 0))
+ expect(project.group.labels.where(type: "GroupLabel").where.not(project_id: nil).count).to eq(0)
end
it 'has group milestone' do
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 08e5bbbd400..5804c45871e 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -164,6 +164,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
end
+ it 'saves the properties for a service' do
+ expect(saved_project_json['services'].first['properties']).to eq('one' => 'value')
+ end
+
it 'has project feature' do
project_feature = saved_project_json['project_feature']
expect(project_feature).not_to be_empty
@@ -279,7 +283,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
commit_id: ci_build.pipeline.sha)
create(:event, :created, target: milestone, project: project, author: user)
- create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
+ create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker', properties: { one: 'value' })
create(:project_custom_attribute, project: project)
create(:project_custom_attribute, project: project)
diff --git a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
index 63992ea8ab8..a685521cbf0 100644
--- a/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_restorer_spec.rb
@@ -4,7 +4,6 @@ describe Gitlab::ImportExport::UploadsRestorer do
describe 'bundle a project Git repo' do
let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) }
- let(:uploads_path) { FileUploader.dynamic_path_segment(project) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
@@ -26,9 +25,9 @@ describe Gitlab::ImportExport::UploadsRestorer do
end
it 'copies the uploads to the project path' do
- restorer.restore
+ subject.restore
- uploads = Dir.glob(File.join(uploads_path, '**/*')).map { |file| File.basename(file) }
+ uploads = Dir.glob(File.join(subject.uploads_path, '**/*')).map { |file| File.basename(file) }
expect(uploads).to include('dummy.txt')
end
@@ -44,9 +43,9 @@ describe Gitlab::ImportExport::UploadsRestorer do
end
it 'copies the uploads to the project path' do
- restorer.restore
+ subject.restore
- uploads = Dir.glob(File.join(uploads_path, '**/*')).map { |file| File.basename(file) }
+ uploads = Dir.glob(File.join(subject.uploads_path, '**/*')).map { |file| File.basename(file) }
expect(uploads).to include('dummy.txt')
end
diff --git a/spec/lib/gitlab/import_export/uploads_saver_spec.rb b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
index e8948de1f3a..959779523f4 100644
--- a/spec/lib/gitlab/import_export/uploads_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/uploads_saver_spec.rb
@@ -30,7 +30,7 @@ describe Gitlab::ImportExport::UploadsSaver do
it 'copies the uploads to the export path' do
saver.save
- uploads = Dir.glob(File.join(shared.export_path, 'uploads', '**/*')).map { |file| File.basename(file) }
+ uploads = Dir.glob(File.join(saver.uploads_export_path, '**/*')).map { |file| File.basename(file) }
expect(uploads).to include('banana_sample.gif')
end
@@ -52,7 +52,7 @@ describe Gitlab::ImportExport::UploadsSaver do
it 'copies the uploads to the export path' do
saver.save
- uploads = Dir.glob(File.join(shared.export_path, 'uploads', '**/*')).map { |file| File.basename(file) }
+ uploads = Dir.glob(File.join(saver.uploads_export_path, '**/*')).map { |file| File.basename(file) }
expect(uploads).to include('banana_sample.gif')
end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index 0b8e97b8948..ebb6033f71e 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -63,14 +63,14 @@ describe Gitlab::Kubernetes::Helm::Pod do
it 'should mount configMap specification in the volume' do
spec = subject.generate.spec
- expect(spec.volumes.first.configMap['name']).to eq('values-content-configuration')
+ expect(spec.volumes.first.configMap['name']).to eq("values-content-configuration-#{app.name}")
expect(spec.volumes.first.configMap['items'].first['key']).to eq('values')
expect(spec.volumes.first.configMap['items'].first['path']).to eq('values.yaml')
end
end
context 'without a configuration file' do
- let(:app) { create(:clusters_applications_ingress, cluster: cluster) }
+ let(:app) { create(:clusters_applications_helm, cluster: cluster) }
it_behaves_like 'helm pod'
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 1785094af10..9c30ddd7fe2 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe Gitlab::LDAP::AuthHash do
+ include LdapHelpers
+
let(:auth_hash) do
described_class.new(
OmniAuth::AuthHash.new(
@@ -83,4 +85,26 @@ describe Gitlab::LDAP::AuthHash do
end
end
end
+
+ describe '#username' do
+ context 'if lowercase_usernames setting is' do
+ let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' }
+
+ before do
+ raw_info[:uid] = ['JOHN']
+ end
+
+ it 'enabled the username attribute is lower cased' do
+ stub_ldap_config(lowercase_usernames: true)
+
+ expect(auth_hash.username).to eq 'john'
+ end
+
+ it 'disabled the username attribute is not lower cased' do
+ stub_ldap_config(lowercase_usernames: false)
+
+ expect(auth_hash.username).to eq 'JOHN'
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb
index ff29d9aa5be..b54d4000b53 100644
--- a/spec/lib/gitlab/ldap/person_spec.rb
+++ b/spec/lib/gitlab/ldap/person_spec.rb
@@ -139,6 +139,27 @@ describe Gitlab::LDAP::Person do
expect(person.username).to eq(attr_value)
end
end
+
+ context 'if lowercase_usernames setting is' do
+ let(:username_attribute) { 'uid' }
+
+ before do
+ entry[username_attribute] = 'JOHN'
+ @person = described_class.new(entry, 'ldapmain')
+ end
+
+ it 'enabled the username attribute is lower cased' do
+ stub_ldap_config(lowercase_usernames: true)
+
+ expect(@person.username).to eq 'john'
+ end
+
+ it 'disabled the username attribute is not lower cased' do
+ stub_ldap_config(lowercase_usernames: false)
+
+ expect(@person.username).to eq 'JOHN'
+ end
+ end
end
def assert_generic_test(test_description, got, expected)
diff --git a/spec/lib/gitlab/metrics/method_call_spec.rb b/spec/lib/gitlab/metrics/method_call_spec.rb
index 41a9d1d9c90..d9379cfe674 100644
--- a/spec/lib/gitlab/metrics/method_call_spec.rb
+++ b/spec/lib/gitlab/metrics/method_call_spec.rb
@@ -5,6 +5,10 @@ describe Gitlab::Metrics::MethodCall do
let(:method_call) { described_class.new('Foo#bar', :Foo, '#bar', transaction) }
describe '#measure' do
+ after do
+ described_class.reload_metric!(:gitlab_method_call_duration_seconds)
+ end
+
it 'measures the performance of the supplied block' do
method_call.measure { 'foo' }
@@ -20,8 +24,6 @@ describe Gitlab::Metrics::MethodCall do
context 'prometheus instrumentation is enabled' do
before do
- allow(Feature.get(:prometheus_metrics_method_instrumentation)).to receive(:enabled?).and_call_original
- described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1
Feature.get(:prometheus_metrics_method_instrumentation).enable
end
@@ -31,30 +33,12 @@ describe Gitlab::Metrics::MethodCall do
end
end
- it 'caches subsequent invocations of feature check' do
- 10.times do
- method_call.measure { 'foo' }
- end
-
- expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).once
- end
-
- it 'expires feature check cache after 1 minute' do
- method_call.measure { 'foo' }
-
- Timecop.travel(1.minute.from_now) do
- method_call.measure { 'foo' }
- end
-
- Timecop.travel(1.minute.from_now + 1.second) do
- method_call.measure { 'foo' }
- end
-
- expect(Feature.get(:prometheus_metrics_method_instrumentation)).to have_received(:enabled?).twice
+ it 'metric is not a NullMetric' do
+ expect(described_class).not_to be_instance_of(Gitlab::Metrics::NullMetric)
end
it 'observes the performance of the supplied block' do
- expect(described_class.call_duration_histogram)
+ expect(described_class.gitlab_method_call_duration_seconds)
.to receive(:observe)
.with({ module: :Foo, method: '#bar' }, be_a_kind_of(Numeric))
@@ -64,14 +48,12 @@ describe Gitlab::Metrics::MethodCall do
context 'prometheus instrumentation is disabled' do
before do
- described_class.measurement_enabled_cache_expires_at.value = Time.now.to_i - 1
-
Feature.get(:prometheus_metrics_method_instrumentation).disable
end
- it 'does not observe the performance' do
- expect(described_class.call_duration_histogram)
- .not_to receive(:observe)
+ it 'observes using NullMetric' do
+ expect(described_class.gitlab_method_call_duration_seconds).to be_instance_of(Gitlab::Metrics::NullMetric)
+ expect(described_class.gitlab_method_call_duration_seconds).to receive(:observe)
method_call.measure { 'foo' }
end
@@ -81,12 +63,10 @@ describe Gitlab::Metrics::MethodCall do
context 'when measurement is below threshold' do
before do
allow(method_call).to receive(:above_threshold?).and_return(false)
-
- Feature.get(:prometheus_metrics_method_instrumentation).enable
end
it 'does not observe the performance' do
- expect(described_class.call_duration_histogram)
+ expect(described_class.gitlab_method_call_duration_seconds)
.not_to receive(:observe)
method_call.measure { 'foo' }
@@ -96,7 +76,7 @@ describe Gitlab::Metrics::MethodCall do
describe '#to_metric' do
it 'returns a Metric instance' do
- expect(method_call).to receive(:real_time).and_return(4.0001)
+ expect(method_call).to receive(:real_time).and_return(4.0001).twice
expect(method_call).to receive(:cpu_time).and_return(3.0001)
method_call.measure { 'foo' }
diff --git a/spec/lib/gitlab/metrics/methods_spec.rb b/spec/lib/gitlab/metrics/methods_spec.rb
new file mode 100644
index 00000000000..9d41ed2442b
--- /dev/null
+++ b/spec/lib/gitlab/metrics/methods_spec.rb
@@ -0,0 +1,137 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Methods do
+ subject { Class.new { include Gitlab::Metrics::Methods } }
+
+ shared_context 'metric' do |metric_type, *args|
+ let(:docstring) { 'description' }
+ let(:metric_name) { :sample_metric }
+
+ describe "#define_#{metric_type}" do
+ define_method(:call_define_metric_method) do |**args|
+ subject.__send__("define_#{metric_type}", metric_name, **args)
+ end
+
+ context 'metrics access method not defined' do
+ it "defines metrics accessing method" do
+ expect(subject).not_to respond_to(metric_name)
+
+ call_define_metric_method(docstring: docstring)
+
+ expect(subject).to respond_to(metric_name)
+ end
+ end
+
+ context 'metrics access method defined' do
+ before do
+ call_define_metric_method(docstring: docstring)
+ end
+
+ it 'raises error when trying to redefine method' do
+ expect { call_define_metric_method(docstring: docstring) }.to raise_error(ArgumentError)
+ end
+
+ context 'metric is not cached' do
+ it 'calls fetch_metric' do
+ expect(subject).to receive(:init_metric).with(metric_type, metric_name, docstring: docstring)
+
+ subject.public_send(metric_name)
+ end
+ end
+
+ context 'metric is cached' do
+ before do
+ subject.public_send(metric_name)
+ end
+
+ it 'returns cached metric' do
+ expect(subject).not_to receive(:init_metric)
+
+ subject.public_send(metric_name)
+ end
+ end
+ end
+ end
+
+ describe "#fetch_#{metric_type}" do
+ let(:null_metric) { Gitlab::Metrics::NullMetric.instance }
+
+ define_method(:call_fetch_metric_method) do |**args|
+ subject.__send__("fetch_#{metric_type}", metric_name, **args)
+ end
+
+ context "when #{metric_type} is not cached" do
+ it 'initializes counter metric' do
+ allow(Gitlab::Metrics).to receive(metric_type).and_return(null_metric)
+
+ call_fetch_metric_method(docstring: docstring)
+
+ expect(Gitlab::Metrics).to have_received(metric_type).with(metric_name, docstring, *args)
+ end
+ end
+
+ context "when #{metric_type} is cached" do
+ before do
+ call_fetch_metric_method(docstring: docstring)
+ end
+
+ it 'uses class metric cache' do
+ expect(Gitlab::Metrics).not_to receive(metric_type)
+
+ call_fetch_metric_method(docstring: docstring)
+ end
+
+ context 'when metric is reloaded' do
+ before do
+ subject.reload_metric!(metric_name)
+ end
+
+ it "initializes #{metric_type} metric" do
+ allow(Gitlab::Metrics).to receive(metric_type).and_return(null_metric)
+
+ call_fetch_metric_method(docstring: docstring)
+
+ expect(Gitlab::Metrics).to have_received(metric_type).with(metric_name, docstring, *args)
+ end
+ end
+ end
+
+ context 'when metric is configured with feature' do
+ let(:feature_name) { :some_metric_feature }
+ let(:metric) { call_fetch_metric_method(docstring: docstring, with_feature: feature_name) }
+
+ context 'when feature is enabled' do
+ before do
+ Feature.get(feature_name).enable
+ end
+
+ it "initializes #{metric_type} metric" do
+ allow(Gitlab::Metrics).to receive(metric_type).and_return(null_metric)
+
+ metric
+
+ expect(Gitlab::Metrics).to have_received(metric_type).with(metric_name, docstring, *args)
+ end
+ end
+
+ context 'when feature is disabled' do
+ before do
+ Feature.get(feature_name).disable
+ end
+
+ it "returns NullMetric" do
+ allow(Gitlab::Metrics).to receive(metric_type)
+
+ expect(metric).to be_instance_of(Gitlab::Metrics::NullMetric)
+
+ expect(Gitlab::Metrics).not_to have_received(metric_type)
+ end
+ end
+ end
+ end
+ end
+
+ include_examples 'metric', :counter, {}
+ include_examples 'metric', :gauge, {}, :all
+ include_examples 'metric', :histogram, {}, [0.005, 0.01, 0.1, 1, 10]
+end
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 375cbf8a9ca..54781dd52fc 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -2,6 +2,11 @@ require 'spec_helper'
describe Gitlab::Metrics::Samplers::RubySampler do
let(:sampler) { 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
after do
Allocations.stop if Gitlab::Metrics.mri?
@@ -17,12 +22,9 @@ describe Gitlab::Metrics::Samplers::RubySampler do
end
it 'adds a metric containing the memory usage' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage)
- .and_return(9000)
+ expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000)
- expect(sampler.metrics[:memory_usage]).to receive(:set)
- .with({}, 9000)
- .and_call_original
+ expect(sampler.metrics[:memory_usage]).to receive(:set).with({}, 9000)
sampler.sample
end
@@ -31,9 +33,7 @@ describe Gitlab::Metrics::Samplers::RubySampler do
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
.and_return(4)
- expect(sampler.metrics[:file_descriptors]).to receive(:set)
- .with({}, 4)
- .and_call_original
+ expect(sampler.metrics[:file_descriptors]).to receive(:set).with({}, 4)
sampler.sample
end
@@ -49,16 +49,14 @@ describe Gitlab::Metrics::Samplers::RubySampler do
it 'adds a metric containing garbage collection time statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
- expect(sampler.metrics[:total_time]).to receive(:set)
- .with({}, 240)
- .and_call_original
+ expect(sampler.metrics[:total_time]).to receive(:set).with({}, 240)
sampler.sample
end
it 'adds a metric containing garbage collection statistics' do
GC.stat.keys.each do |key|
- expect(sampler.metrics[key]).to receive(:set).with({}, anything).and_call_original
+ expect(sampler.metrics[key]).to receive(:set).with({}, anything)
end
sampler.sample
diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
index eca75a4fac1..9f3af1acef7 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -32,7 +32,7 @@ describe Gitlab::Metrics::Subscribers::ActionView do
end
it 'observes view rendering time' do
- expect(subscriber.send(:metric_view_rendering_duration_seconds))
+ expect(described_class.gitlab_view_rendering_duration_seconds)
.to receive(:observe)
.with({ view: 'app/views/x.html.haml' }, 2.1)
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 9b3698fb4a8..4e7bd433a9c 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::Metrics::Subscribers::ActiveRecord do
expect(subscriber).to receive(:current_transaction)
.at_least(:once)
.and_return(transaction)
- expect(subscriber.send(:metric_sql_duration_seconds)).to receive(:observe).with({}, 0.002)
+ expect(described_class.send(:gitlab_sql_duration_seconds)).to receive(:observe).with({}, 0.002)
subscriber.sql(event)
end
diff --git a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
index 58e28592cf9..6795c1ab56b 100644
--- a/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/rails_cache_spec.rb
@@ -144,7 +144,10 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
end
context 'with a transaction' do
+ let(:metric_cache_misses_total) { double('metric_cache_misses_total', increment: nil) }
+
before do
+ allow(subscriber).to receive(:metric_cache_misses_total).and_return(metric_cache_misses_total)
allow(subscriber).to receive(:current_transaction)
.and_return(transaction)
end
@@ -157,9 +160,9 @@ describe Gitlab::Metrics::Subscribers::RailsCache do
end
it 'increments the cache_read_miss total' do
- expect(subscriber.send(:metric_cache_misses_total)).to receive(:increment).with({})
-
subscriber.cache_generate(event)
+
+ expect(metric_cache_misses_total).to have_received(:increment).with({})
end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 1619fbd88b1..03c185ddc07 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -20,7 +20,7 @@ describe Gitlab::Metrics do
context 'prometheus metrics enabled in config' do
before do
- allow(described_class).to receive(:current_application_settings).and_return(prometheus_metrics_enabled: true)
+ allow(Gitlab::CurrentSettings).to receive(:prometheus_metrics_enabled).and_return(true)
end
context 'when metrics folder is present' do
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 45fff4c5787..03e0a9e2a03 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -44,6 +44,18 @@ describe Gitlab::OAuth::User do
let(:provider) { 'twitter' }
+ describe 'when account exists on server' do
+ it 'does not mark the user as external' do
+ create(:omniauth_user, extern_uid: 'my-uid', provider: provider)
+ stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
+
+ oauth_user.save
+
+ expect(gl_user).to be_valid
+ expect(gl_user.external).to be_falsey
+ end
+ end
+
describe 'signup' do
context 'when signup is disabled' do
before do
@@ -51,7 +63,7 @@ describe Gitlab::OAuth::User do
end
it 'creates the user' do
- stub_omniauth_config(allow_single_sign_on: ['twitter'])
+ stub_omniauth_config(allow_single_sign_on: [provider])
oauth_user.save
@@ -65,7 +77,7 @@ describe Gitlab::OAuth::User do
end
it 'creates and confirms the user anyway' do
- stub_omniauth_config(allow_single_sign_on: ['twitter'])
+ stub_omniauth_config(allow_single_sign_on: [provider])
oauth_user.save
@@ -75,7 +87,7 @@ describe Gitlab::OAuth::User do
end
it 'marks user as having password_automatically_set' do
- stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
+ stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
oauth_user.save
@@ -86,7 +98,7 @@ describe Gitlab::OAuth::User do
shared_examples 'to verify compliance with allow_single_sign_on' do
context 'provider is marked as external' do
it 'marks user as external' do
- stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['twitter'])
+ stub_omniauth_config(allow_single_sign_on: [provider], external_providers: [provider])
oauth_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
@@ -95,8 +107,8 @@ describe Gitlab::OAuth::User do
context 'provider was external, now has been removed' do
it 'does not mark external user as internal' do
- create(:omniauth_user, extern_uid: 'my-uid', provider: 'twitter', external: true)
- stub_omniauth_config(allow_single_sign_on: ['twitter'], external_providers: ['facebook'])
+ create(:omniauth_user, extern_uid: 'my-uid', provider: provider, external: true)
+ stub_omniauth_config(allow_single_sign_on: [provider], external_providers: ['facebook'])
oauth_user.save
expect(gl_user).to be_valid
expect(gl_user.external).to be_truthy
@@ -118,7 +130,7 @@ describe Gitlab::OAuth::User do
context 'with new allow_single_sign_on enabled syntax' do
before do
- stub_omniauth_config(allow_single_sign_on: ['twitter'])
+ stub_omniauth_config(allow_single_sign_on: [provider])
end
it "creates a user from Omniauth" do
@@ -127,7 +139,7 @@ describe Gitlab::OAuth::User do
expect(gl_user).to be_valid
identity = gl_user.identities.first
expect(identity.extern_uid).to eql uid
- expect(identity.provider).to eql 'twitter'
+ expect(identity.provider).to eql provider
end
end
@@ -142,7 +154,7 @@ describe Gitlab::OAuth::User do
expect(gl_user).to be_valid
identity = gl_user.identities.first
expect(identity.extern_uid).to eql uid
- expect(identity.provider).to eql 'twitter'
+ expect(identity.provider).to eql provider
end
end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 0ae90069b7f..85991c38363 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -121,7 +121,7 @@ describe Gitlab::PathRegex do
STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
- WILDCARD_SEGMENT = %r{\*}
+ WILDCARD_SEGMENT = /\*/
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}}
diff --git a/spec/lib/gitlab/popen/runner_spec.rb b/spec/lib/gitlab/popen/runner_spec.rb
new file mode 100644
index 00000000000..2e2cb4ca28f
--- /dev/null
+++ b/spec/lib/gitlab/popen/runner_spec.rb
@@ -0,0 +1,139 @@
+require 'spec_helper'
+
+describe Gitlab::Popen::Runner do
+ subject { described_class.new }
+
+ describe '#run' do
+ it 'runs the command and returns the result' do
+ run_command
+
+ expect(Gitlab::Popen).to have_received(:popen_with_detail)
+ end
+ end
+
+ describe '#all_success_and_clean?' do
+ it 'returns true when exit status is 0 and stderr is empty' do
+ run_command
+
+ expect(subject).to be_all_success_and_clean
+ end
+
+ it 'returns false when exit status is not 0' do
+ run_command(exitstatus: 1)
+
+ expect(subject).not_to be_all_success_and_clean
+ end
+
+ it 'returns false when exit stderr has something' do
+ run_command(stderr: 'stderr')
+
+ expect(subject).not_to be_all_success_and_clean
+ end
+ end
+
+ describe '#all_success?' do
+ it 'returns true when exit status is 0' do
+ run_command
+
+ expect(subject).to be_all_success
+ end
+
+ it 'returns false when exit status is not 0' do
+ run_command(exitstatus: 1)
+
+ expect(subject).not_to be_all_success
+ end
+
+ it 'returns true' do
+ run_command(stderr: 'stderr')
+
+ expect(subject).to be_all_success
+ end
+ end
+
+ describe '#all_stderr_empty?' do
+ it 'returns true when stderr is empty' do
+ run_command
+
+ expect(subject).to be_all_stderr_empty
+ end
+
+ it 'returns true when exit status is not 0' do
+ run_command(exitstatus: 1)
+
+ expect(subject).to be_all_stderr_empty
+ end
+
+ it 'returns false when exit stderr has something' do
+ run_command(stderr: 'stderr')
+
+ expect(subject).not_to be_all_stderr_empty
+ end
+ end
+
+ describe '#failed_results' do
+ it 'returns [] when everything is passed' do
+ run_command
+
+ expect(subject.failed_results).to be_empty
+ end
+
+ it 'returns the result when exit status is not 0' do
+ result = run_command(exitstatus: 1)
+
+ expect(subject.failed_results).to contain_exactly(result)
+ end
+
+ it 'returns [] when exit stderr has something' do
+ run_command(stderr: 'stderr')
+
+ expect(subject.failed_results).to be_empty
+ end
+ end
+
+ describe '#warned_results' do
+ it 'returns [] when everything is passed' do
+ run_command
+
+ expect(subject.warned_results).to be_empty
+ end
+
+ it 'returns [] when exit status is not 0' do
+ run_command(exitstatus: 1)
+
+ expect(subject.warned_results).to be_empty
+ end
+
+ it 'returns the result when exit stderr has something' do
+ result = run_command(stderr: 'stderr')
+
+ expect(subject.warned_results).to contain_exactly(result)
+ end
+ end
+
+ def run_command(
+ command: 'command',
+ stdout: 'stdout',
+ stderr: '',
+ exitstatus: 0,
+ status: double(exitstatus: exitstatus, success?: exitstatus.zero?),
+ duration: 0.1)
+
+ result =
+ Gitlab::Popen::Result.new(command, stdout, stderr, status, duration)
+
+ allow(Gitlab::Popen)
+ .to receive(:popen_with_detail)
+ .and_return(result)
+
+ subject.run([command]) do |cmd, &run|
+ expect(cmd).to eq(command)
+
+ cmd_result = run.call
+
+ expect(cmd_result).to eq(result)
+ end
+
+ subject.results.first
+ end
+end
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index b145ca36f26..1dbead16d5b 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -1,11 +1,23 @@
require 'spec_helper'
-describe 'Gitlab::Popen' do
+describe Gitlab::Popen do
let(:path) { Rails.root.join('tmp').to_s }
before do
@klass = Class.new(Object)
- @klass.send(:include, Gitlab::Popen)
+ @klass.send(:include, described_class)
+ end
+
+ describe '.popen_with_detail' do
+ subject { @klass.new.popen_with_detail(cmd) }
+
+ let(:cmd) { %W[#{Gem.ruby} -e $stdout.puts(1);$stderr.puts(2);exit(3)] }
+
+ it { expect(subject.cmd).to eq(cmd) }
+ it { expect(subject.stdout).to eq("1\n") }
+ it { expect(subject.stderr).to eq("2\n") }
+ it { expect(subject.status.exitstatus).to eq(3) }
+ it { expect(subject.duration).to be_kind_of(Numeric) }
end
context 'zero status' do
diff --git a/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb
new file mode 100644
index 00000000000..b49bc5c328c
--- /dev/null
+++ b/spec/lib/gitlab/query_limiting/active_support_subscriber_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::QueryLimiting::ActiveSupportSubscriber do
+ describe '#sql' do
+ it 'increments the number of executed SQL queries' do
+ transaction = double(:transaction)
+
+ allow(Gitlab::QueryLimiting::Transaction)
+ .to receive(:current)
+ .and_return(transaction)
+
+ expect(transaction)
+ .to receive(:increment)
+ .at_least(:once)
+
+ User.count
+ end
+ end
+end
diff --git a/spec/lib/gitlab/query_limiting/middleware_spec.rb b/spec/lib/gitlab/query_limiting/middleware_spec.rb
new file mode 100644
index 00000000000..a04bcdecb4b
--- /dev/null
+++ b/spec/lib/gitlab/query_limiting/middleware_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Gitlab::QueryLimiting::Middleware do
+ describe '#call' do
+ it 'runs the application with query limiting in place' do
+ middleware = described_class.new(-> (env) { env })
+
+ expect_any_instance_of(Gitlab::QueryLimiting::Transaction)
+ .to receive(:act_upon_results)
+
+ expect(middleware.call({ number: 10 }))
+ .to eq({ number: 10 })
+ end
+ end
+
+ describe '#action_name' do
+ let(:middleware) { described_class.new(-> (env) { env }) }
+
+ context 'using a Rails request' do
+ it 'returns the name of the controller and action' do
+ env = {
+ described_class::CONTROLLER_KEY => double(
+ :controller,
+ action_name: 'show',
+ class: double(:class, name: 'UsersController'),
+ content_type: 'text/html'
+ )
+ }
+
+ expect(middleware.action_name(env)).to eq('UsersController#show')
+ end
+
+ it 'includes the content type if this is not text/html' do
+ env = {
+ described_class::CONTROLLER_KEY => double(
+ :controller,
+ action_name: 'show',
+ class: double(:class, name: 'UsersController'),
+ content_type: 'application/json'
+ )
+ }
+
+ expect(middleware.action_name(env))
+ .to eq('UsersController#show (application/json)')
+ end
+ end
+
+ context 'using a Grape API request' do
+ it 'returns the name of the request method and endpoint path' do
+ env = {
+ described_class::ENDPOINT_KEY => double(
+ :endpoint,
+ route: double(:route, request_method: 'GET', path: '/foo')
+ )
+ }
+
+ expect(middleware.action_name(env)).to eq('GET /foo')
+ end
+
+ it 'returns nil if the route can not be retrieved' do
+ endpoint = double(:endpoint)
+ env = { described_class::ENDPOINT_KEY => endpoint }
+
+ allow(endpoint)
+ .to receive(:route)
+ .and_raise(RuntimeError)
+
+ expect(middleware.action_name(env)).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/query_limiting/transaction_spec.rb b/spec/lib/gitlab/query_limiting/transaction_spec.rb
new file mode 100644
index 00000000000..b4231fcd0fa
--- /dev/null
+++ b/spec/lib/gitlab/query_limiting/transaction_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+describe Gitlab::QueryLimiting::Transaction do
+ after do
+ Thread.current[described_class::THREAD_KEY] = nil
+ end
+
+ describe '.current' do
+ it 'returns nil when there is no transaction' do
+ expect(described_class.current).to be_nil
+ end
+
+ it 'returns the transaction when present' do
+ Thread.current[described_class::THREAD_KEY] = described_class.new
+
+ expect(described_class.current).to be_an_instance_of(described_class)
+ end
+ end
+
+ describe '.run' do
+ it 'runs a transaction and returns it and its return value' do
+ trans, ret = described_class.run do
+ 10
+ end
+
+ expect(trans).to be_an_instance_of(described_class)
+ expect(ret).to eq(10)
+ end
+
+ it 'removes the transaction from the current thread upon completion' do
+ described_class.run do
+ 10
+ end
+
+ expect(Thread.current[described_class::THREAD_KEY]).to be_nil
+ end
+ end
+
+ describe '#act_upon_results' do
+ context 'when the query threshold is not exceeded' do
+ it 'does nothing' do
+ trans = described_class.new
+
+ expect(trans).not_to receive(:raise)
+
+ trans.act_upon_results
+ end
+ end
+
+ context 'when the query threshold is exceeded' do
+ let(:transaction) do
+ trans = described_class.new
+ trans.count = described_class::THRESHOLD + 1
+
+ trans
+ end
+
+ it 'raises an error when this is enabled' do
+ expect { transaction.act_upon_results }
+ .to raise_error(described_class::ThresholdExceededError)
+ end
+
+ it 'reports the error in Sentry if raising an error is disabled' do
+ expect(transaction)
+ .to receive(:raise_error?)
+ .and_return(false)
+
+ expect(Raven)
+ .to receive(:capture_exception)
+ .with(an_instance_of(described_class::ThresholdExceededError))
+
+ transaction.act_upon_results
+ end
+ end
+ end
+
+ describe '#increment' do
+ it 'increments the number of executed queries' do
+ transaction = described_class.new
+
+ expect(transaction.count).to be_zero
+
+ transaction.increment
+
+ expect(transaction.count).to eq(1)
+ end
+ end
+
+ describe '#raise_error?' do
+ it 'returns true in a test environment' do
+ transaction = described_class.new
+
+ expect(transaction.raise_error?).to eq(true)
+ end
+
+ it 'returns false in a production environment' do
+ transaction = described_class.new
+
+ expect(Rails.env)
+ .to receive(:test?)
+ .and_return(false)
+
+ expect(transaction.raise_error?).to eq(false)
+ end
+ end
+
+ describe '#threshold_exceeded?' do
+ it 'returns false when the threshold is not exceeded' do
+ transaction = described_class.new
+
+ expect(transaction.threshold_exceeded?).to eq(false)
+ end
+
+ it 'returns true when the threshold is exceeded' do
+ transaction = described_class.new
+ transaction.count = described_class::THRESHOLD + 1
+
+ expect(transaction.threshold_exceeded?).to eq(true)
+ end
+ end
+
+ describe '#error_message' do
+ it 'returns the error message to display when the threshold is exceeded' do
+ transaction = described_class.new
+ transaction.count = max = described_class::THRESHOLD
+
+ expect(transaction.error_message).to eq(
+ "Too many SQL queries were executed: a maximum of #{max} " \
+ "is allowed but #{max} SQL queries were executed"
+ )
+ end
+
+ it 'includes the action name in the error message when present' do
+ transaction = described_class.new
+ transaction.count = max = described_class::THRESHOLD
+ transaction.action = 'UsersController#show'
+
+ expect(transaction.error_message).to eq(
+ "Too many SQL queries were executed in UsersController#show: " \
+ "a maximum of #{max} is allowed but #{max} SQL queries were executed"
+ )
+ end
+ end
+end
diff --git a/spec/lib/gitlab/query_limiting_spec.rb b/spec/lib/gitlab/query_limiting_spec.rb
new file mode 100644
index 00000000000..2eddab0b8c3
--- /dev/null
+++ b/spec/lib/gitlab/query_limiting_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::QueryLimiting do
+ describe '.enable?' do
+ it 'returns true in a test environment' do
+ expect(described_class.enable?).to eq(true)
+ end
+
+ it 'returns true in a development environment' do
+ allow(Rails.env).to receive(:development?).and_return(true)
+
+ expect(described_class.enable?).to eq(true)
+ end
+
+ it 'returns true on GitLab.com' do
+ allow(Gitlab).to receive(:com?).and_return(true)
+
+ expect(described_class.enable?).to eq(true)
+ end
+
+ it 'returns true in a non GitLab.com' do
+ expect(Gitlab).to receive(:com?).and_return(false)
+ expect(Rails.env).to receive(:development?).and_return(false)
+ expect(Rails.env).to receive(:test?).and_return(false)
+
+ expect(described_class.enable?).to eq(false)
+ end
+ end
+
+ describe '.whitelist' do
+ it 'raises ArgumentError when an invalid issue URL is given' do
+ expect { described_class.whitelist('foo') }
+ .to raise_error(ArgumentError)
+ end
+
+ context 'without a transaction' do
+ it 'does nothing' do
+ expect { described_class.whitelist('https://example.com') }
+ .not_to raise_error
+ end
+ end
+
+ context 'with a transaction' do
+ let(:transaction) { Gitlab::QueryLimiting::Transaction.new }
+
+ before do
+ allow(Gitlab::QueryLimiting::Transaction)
+ .to receive(:current)
+ .and_return(transaction)
+ end
+
+ it 'does not increment the number of SQL queries executed in the block' do
+ before = transaction.count
+
+ described_class.whitelist('https://example.com')
+
+ 2.times do
+ User.count
+ end
+
+ expect(transaction.count).to eq(before)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index b5a9ac570e6..17b48b3d062 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -19,6 +19,12 @@ describe Gitlab::SearchResults do
project.add_developer(user)
end
+ describe '#objects' do
+ it 'returns without_page collection by default' do
+ expect(results.objects('projects')).to be_kind_of(Kaminari::PaginatableWithoutCount)
+ end
+ end
+
describe '#projects_count' do
it 'returns the total amount of projects' do
expect(results.projects_count).to eq(1)
@@ -43,6 +49,58 @@ describe Gitlab::SearchResults do
end
end
+ context "when count_limit is lower than total amount" do
+ before do
+ allow(results).to receive(:count_limit).and_return(1)
+ end
+
+ describe '#limited_projects_count' do
+ it 'returns the limited amount of projects' do
+ create(:project, name: 'foo2')
+
+ expect(results.limited_projects_count).to eq(1)
+ end
+ end
+
+ describe '#limited_merge_requests_count' do
+ it 'returns the limited amount of merge requests' do
+ create(:merge_request, :simple, source_project: project, title: 'foo2')
+
+ expect(results.limited_merge_requests_count).to eq(1)
+ end
+ end
+
+ describe '#limited_milestones_count' do
+ it 'returns the limited amount of milestones' do
+ create(:milestone, project: project, title: 'foo2')
+
+ expect(results.limited_milestones_count).to eq(1)
+ end
+ end
+
+ describe '#limited_issues_count' do
+ it 'runs single SQL query to get the limited amount of issues' do
+ create(:milestone, project: project, title: 'foo2')
+
+ expect(results).to receive(:issues).with(public_only: true).and_call_original
+ expect(results).not_to receive(:issues).with(no_args).and_call_original
+
+ expect(results.limited_issues_count).to eq(1)
+ end
+ end
+ end
+
+ context "when count_limit is higher than total amount" do
+ describe '#limited_issues_count' do
+ it 'runs multiple queries to get the limited amount of issues' do
+ expect(results).to receive(:issues).with(public_only: true).and_call_original
+ expect(results).to receive(:issues).with(no_args).and_call_original
+
+ expect(results.limited_issues_count).to eq(1)
+ end
+ end
+ end
+
it 'includes merge requests from source and target projects' do
forked_project = fork_project(project, user)
merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo')
diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
index e41e5254dde..35d01efc1bd 100644
--- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::SlashCommands::IssueSearch do
let!(:issue) { create(:issue, project: project, title: 'find me') }
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
let(:project) { create(:project) }
- let(:user) { issue.author }
+ let(:user) { create(:user) }
let(:regex_match) { described_class.match("issue search find") }
subject do
diff --git a/spec/lib/gitlab/ssh_public_key_spec.rb b/spec/lib/gitlab/ssh_public_key_spec.rb
index 93d538141ce..c15e29774b6 100644
--- a/spec/lib/gitlab/ssh_public_key_spec.rb
+++ b/spec/lib/gitlab/ssh_public_key_spec.rb
@@ -37,6 +37,41 @@ describe Gitlab::SSHPublicKey, lib: true do
end
end
+ describe '.sanitize(key_content)' do
+ let(:content) { build(:key).key }
+
+ context 'when key has blank space characters' do
+ it 'removes the extra blank space characters' do
+ unsanitized = content.insert(100, "\n")
+ .insert(40, "\r\n")
+ .insert(30, ' ')
+
+ sanitized = described_class.sanitize(unsanitized)
+ _, body = sanitized.split
+
+ expect(sanitized).not_to eq(unsanitized)
+ expect(body).not_to match(/\s/)
+ end
+ end
+
+ context "when key doesn't have blank space characters" do
+ it "doesn't modify the content" do
+ sanitized = described_class.sanitize(content)
+
+ expect(sanitized).to eq(content)
+ end
+ end
+
+ context "when key is invalid" do
+ it 'returns the original content' do
+ unsanitized = "ssh-foo any content=="
+ sanitized = described_class.sanitize(unsanitized)
+
+ expect(sanitized).to eq(unsanitized)
+ end
+ end
+ end
+
describe '#valid?' do
subject { public_key }
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index b5f2a15ada3..0e9ecff25a6 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -103,9 +103,9 @@ describe Gitlab::UsageData do
subject { described_class.features_usage_data_ce }
it 'gathers feature usage data' do
- expect(subject[:signup]).to eq(current_application_settings.allow_signup?)
+ expect(subject[:signup]).to eq(Gitlab::CurrentSettings.allow_signup?)
expect(subject[:ldap]).to eq(Gitlab.config.ldap.enabled)
- expect(subject[:gravatar]).to eq(current_application_settings.gravatar_enabled?)
+ expect(subject[:gravatar]).to eq(Gitlab::CurrentSettings.gravatar_enabled?)
expect(subject[:omniauth]).to eq(Gitlab.config.omniauth.enabled)
expect(subject[:reply_by_email]).to eq(Gitlab::IncomingEmail.enabled?)
expect(subject[:container_registry]).to eq(Gitlab.config.registry.enabled)
@@ -129,7 +129,7 @@ describe Gitlab::UsageData do
subject { described_class.license_usage_data }
it "gathers license data" do
- expect(subject[:uuid]).to eq(current_application_settings.uuid)
+ expect(subject[:uuid]).to eq(Gitlab::CurrentSettings.uuid)
expect(subject[:version]).to eq(Gitlab::VERSION)
expect(subject[:active_user_count]).to eq(User.active.count)
expect(subject[:recorded_at]).to be_a(Time)
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
index d85dac630b4..2c1146ceff5 100644
--- a/spec/lib/gitlab/visibility_level_spec.rb
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -57,6 +57,15 @@ describe Gitlab::VisibilityLevel do
expect(described_class.allowed_levels)
.to contain_exactly(described_class::PRIVATE, described_class::PUBLIC)
end
+
+ it 'returns all levels when no visibility level was set' do
+ allow(described_class)
+ .to receive_message_chain('current_application_settings.restricted_visibility_levels')
+ .and_return(nil)
+
+ expect(described_class.allowed_levels)
+ .to contain_exactly(described_class::PRIVATE, described_class::INTERNAL, described_class::PUBLIC)
+ end
end
describe '.closest_allowed_level' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 7a8e798e3c9..59eda025108 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -1357,7 +1357,7 @@ describe Notify do
matcher :have_part_with do |expected|
match do |actual|
- actual.body.parts.any? { |part| part.content_type.try(:match, %r(#{expected})) }
+ actual.body.parts.any? { |part| part.content_type.try(:match, /#{expected}/) }
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
new file mode 100644
index 00000000000..4a22bd6f342
--- /dev/null
+++ b/spec/migrations/add_foreign_keys_to_todos_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+require Rails.root.join('db', 'migrate', '20180201110056_add_foreign_keys_to_todos.rb')
+
+describe AddForeignKeysToTodos, :migration do
+ let(:todos) { table(:todos) }
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ context 'add foreign key on user_id' do
+ let!(:todo_with_user) { create_todo(user_id: user.id) }
+ let!(:todo_without_user) { create_todo(user_id: 4711) }
+
+ it 'removes orphaned todos without corresponding user' do
+ expect { migrate! }.to change { Todo.count }.from(2).to(1)
+ end
+
+ it 'does not remove entries with valid user_id' do
+ expect { migrate! }.not_to change { todo_with_user.reload }
+ end
+ end
+
+ context 'add foreign key on author_id' do
+ let!(:todo_with_author) { create_todo(author_id: user.id) }
+ let!(:todo_with_invalid_author) { create_todo(author_id: 4711) }
+
+ it 'removes orphaned todos by author_id' do
+ expect { migrate! }.to change { Todo.count }.from(2).to(1)
+ end
+
+ it 'does not touch author_id for valid entries' do
+ expect { migrate! }.not_to change { todo_with_author.reload }
+ end
+ end
+
+ context 'add foreign key on note_id' do
+ let(:note) { create(:note) }
+ 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) }
+
+ it 'deletes todo if note_id is set but does not exist in notes table' do
+ expect { migrate! }.to change { Todo.count }.from(3).to(2)
+ end
+
+ it 'does not touch entry if note_id is nil' do
+ expect { migrate! }.not_to change { todo_without_note.reload }
+ end
+
+ it 'does not touch note_id for valid entries' do
+ expect { migrate! }.not_to change { todo_with_note.reload }
+ end
+ end
+
+ def create_todo(**opts)
+ todos.create!(
+ project_id: project.id,
+ user_id: user.id,
+ author_id: user.id,
+ target_type: '',
+ action: 0,
+ state: '', **opts
+ )
+ end
+end
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 84c2e9f7e52..63defcb39bf 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,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb')
-describe AddHeadPipelineForEachMergeRequest, :truncate do
+describe AddHeadPipelineForEachMergeRequest, :delete do
include ProjectForksHelper
let(:migration) { described_class.new }
diff --git a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
index 597d8eab51c..f3a46025376 100644
--- a/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
+++ b/spec/migrations/calculate_conv_dev_index_percentages_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170803090603_calculate_conv_dev_index_percentages.rb')
-describe CalculateConvDevIndexPercentages, truncate: true do
+describe CalculateConvDevIndexPercentages, :delete do
let(:migration) { described_class.new }
let!(:conv_dev_index) do
create(:conversational_development_index_metric,
diff --git a/spec/migrations/fix_wrongly_renamed_routes_spec.rb b/spec/migrations/fix_wrongly_renamed_routes_spec.rb
index 78f8ab7512d..543cf55f076 100644
--- a/spec/migrations/fix_wrongly_renamed_routes_spec.rb
+++ b/spec/migrations/fix_wrongly_renamed_routes_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170518231126_fix_wrongly_renamed_routes.rb')
-describe FixWronglyRenamedRoutes, :truncate, :migration do
+describe FixWronglyRenamedRoutes, :migration do
let(:subject) { described_class.new }
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
index cfd4021fbac..ff0d44e1ed2 100644
--- a/spec/migrations/migrate_issues_to_ghost_user_spec.rb
+++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
@@ -8,10 +8,10 @@ describe MigrateIssuesToGhostUser, :migration do
let(:users) { table(:users) }
before do
- projects.create!(name: 'gitlab')
+ project = projects.create!(name: 'gitlab')
user = users.create(email: 'test@example.com')
- issues.create(title: 'Issue 1', author_id: nil, project_id: 1)
- issues.create(title: 'Issue 2', author_id: user.id, project_id: 1)
+ issues.create(title: 'Issue 1', author_id: nil, project_id: project.id)
+ issues.create(title: 'Issue 2', author_id: user.id, project_id: project.id)
end
context 'when ghost user exists' do
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 063829be546..a17c9c72bde 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,7 +3,7 @@
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, :truncate do
+describe MigrateUserActivitiesToUsersLastActivityOn, :clean_gitlab_redis_shared_state, :delete do
let(:migration) { described_class.new }
let!(:user_active_1) { create(:user) }
let!(:user_active_2) { create(:user) }
diff --git a/spec/migrations/migrate_user_project_view_spec.rb b/spec/migrations/migrate_user_project_view_spec.rb
index 5e16769d63a..31d16e17d7b 100644
--- a/spec/migrations/migrate_user_project_view_spec.rb
+++ b/spec/migrations/migrate_user_project_view_spec.rb
@@ -3,7 +3,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170406142253_migrate_user_project_view.rb')
-describe MigrateUserProjectView, :truncate do
+describe MigrateUserProjectView, :delete do
let(:migration) { described_class.new }
let!(:user) { create(:user, project_view: 'readme') }
diff --git a/spec/migrations/remove_duplicate_mr_events_spec.rb b/spec/migrations/remove_duplicate_mr_events_spec.rb
index e393374028f..e51872239ad 100644
--- a/spec/migrations/remove_duplicate_mr_events_spec.rb
+++ b/spec/migrations/remove_duplicate_mr_events_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170815060945_remove_duplicate_mr_events.rb')
-describe RemoveDuplicateMrEvents, truncate: true do
+describe RemoveDuplicateMrEvents, :delete do
let(:migration) { described_class.new }
describe '#up' do
diff --git a/spec/migrations/remove_project_labels_group_id_spec.rb b/spec/migrations/remove_project_labels_group_id_spec.rb
new file mode 100644
index 00000000000..d80d61af20b
--- /dev/null
+++ b/spec/migrations/remove_project_labels_group_id_spec.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180202111106_remove_project_labels_group_id.rb')
+
+describe RemoveProjectLabelsGroupId, :delete do
+ let(:migration) { described_class.new }
+ let(:group) { create(:group) }
+ let!(:project_label) { create(:label, group_id: group.id) }
+ let!(:group_label) { create(:group_label) }
+
+ describe '#up' do
+ it 'updates the project labels group ID' do
+ expect { migration.up }.to change { project_label.reload.group_id }.to(nil)
+ end
+
+ it 'keeps the group labels group ID' do
+ expect { migration.up }.not_to change { group_label.reload.group_id }
+ end
+ end
+end
diff --git a/spec/migrations/rename_more_reserved_project_names_spec.rb b/spec/migrations/rename_more_reserved_project_names_spec.rb
index ae3a4cb9b29..75310075cc5 100644
--- a/spec/migrations/rename_more_reserved_project_names_spec.rb
+++ b/spec/migrations/rename_more_reserved_project_names_spec.rb
@@ -5,8 +5,8 @@ require Rails.root.join('db', 'post_migrate', '20170313133418_rename_more_reserv
# This migration uses multiple threads, and thus different transactions. This
# means data created in this spec may not be visible to some threads. To work
-# around this we use the TRUNCATE cleaning strategy.
-describe RenameMoreReservedProjectNames, truncate: true do
+# around this we use the DELETE cleaning strategy.
+describe RenameMoreReservedProjectNames, :delete do
let(:migration) { described_class.new }
let!(:project) { create(:project) }
diff --git a/spec/migrations/rename_reserved_project_names_spec.rb b/spec/migrations/rename_reserved_project_names_spec.rb
index 462f4c08d63..e6555b1fe6b 100644
--- a/spec/migrations/rename_reserved_project_names_spec.rb
+++ b/spec/migrations/rename_reserved_project_names_spec.rb
@@ -5,8 +5,8 @@ require Rails.root.join('db', 'post_migrate', '20161221153951_rename_reserved_pr
# This migration uses multiple threads, and thus different transactions. This
# means data created in this spec may not be visible to some threads. To work
-# around this we use the TRUNCATE cleaning strategy.
-describe RenameReservedProjectNames, truncate: true do
+# around this we use the DELETE cleaning strategy.
+describe RenameReservedProjectNames, :delete do
let(:migration) { described_class.new }
let!(:project) { create(:project) }
diff --git a/spec/migrations/rename_users_with_renamed_namespace_spec.rb b/spec/migrations/rename_users_with_renamed_namespace_spec.rb
index 1e9aab3d9a1..cbc0ebeb44d 100644
--- a/spec/migrations/rename_users_with_renamed_namespace_spec.rb
+++ b/spec/migrations/rename_users_with_renamed_namespace_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170518200835_rename_users_with_renamed_namespace.rb')
-describe RenameUsersWithRenamedNamespace, truncate: true do
+describe RenameUsersWithRenamedNamespace, :delete do
it 'renames a user that had their namespace renamed to the namespace path' do
other_user = create(:user, username: 'kodingu')
other_user1 = create(:user, username: 'api0')
diff --git a/spec/migrations/update_retried_for_ci_build_spec.rb b/spec/migrations/update_retried_for_ci_build_spec.rb
index 3742b4dafe5..ccb77766b84 100644
--- a/spec/migrations/update_retried_for_ci_build_spec.rb
+++ b/spec/migrations/update_retried_for_ci_build_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170503004427_update_retried_for_ci_build.rb')
-describe UpdateRetriedForCiBuild, truncate: true do
+describe UpdateRetriedForCiBuild, :delete do
let(:pipeline) { create(:ci_pipeline) }
let!(:build_old) { create(:ci_build, pipeline: pipeline, name: 'test') }
let!(:build_new) { create(:ci_build, pipeline: pipeline, name: 'test') }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 45a606c1ea8..f5b3b4a9fc5 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -277,7 +277,7 @@ describe Ci::Build do
allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1)
end
- it { is_expected.to be_an(Array).and all(include(key: "key:1")) }
+ it { is_expected.to be_an(Array).and all(include(key: "key_1")) }
end
context 'when project does not have jobs_cache_index' do
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index f8a98b43e46..959383ff0b7 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -228,7 +228,7 @@ eos
it { expect(data).to be_a(Hash) }
it { expect(data[:message]).to include('adds bar folder and branch-test text file to check Repository merged_to_root_ref method') }
it { expect(data[:timestamp]).to eq('2016-09-27T14:37:46Z') }
- it { expect(data[:added]).to eq(["bar/branch-test.txt"]) }
+ it { expect(data[:added]).to contain_exactly("bar/branch-test.txt") }
it { expect(data[:modified]).to eq([]) }
it { expect(data[:removed]).to eq([]) }
end
@@ -532,8 +532,8 @@ eos
let(:commit2) { merge_request1.merge_request_diff.commits.first }
it 'returns merge_requests that introduced that commit' do
- expect(commit1.merge_requests).to eq([merge_request1, merge_request2])
- expect(commit2.merge_requests).to eq([merge_request1])
+ expect(commit1.merge_requests).to contain_exactly(merge_request1, merge_request2)
+ expect(commit2.merge_requests).to contain_exactly(merge_request1)
end
end
end
diff --git a/spec/models/concerns/avatarable_spec.rb b/spec/models/concerns/avatarable_spec.rb
index cbdc438be0b..3696e6f62fd 100644
--- a/spec/models/concerns/avatarable_spec.rb
+++ b/spec/models/concerns/avatarable_spec.rb
@@ -1,11 +1,11 @@
require 'spec_helper'
describe Avatarable do
- subject { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
+ set(:project) { create(:project, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png'))) }
let(:gitlab_host) { "https://gitlab.example.com" }
let(:relative_url_root) { "/gitlab" }
- let(:asset_host) { "https://gitlab-assets.example.com" }
+ let(:asset_host) { 'https://gitlab-assets.example.com' }
before do
stub_config_setting(base_url: gitlab_host)
@@ -15,29 +15,32 @@ describe Avatarable do
describe '#avatar_path' do
using RSpec::Parameterized::TableSyntax
- where(:has_asset_host, :visibility_level, :only_path, :avatar_path) do
- true | Project::PRIVATE | true | [gitlab_host, relative_url_root, subject.avatar.url]
- true | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url]
- true | Project::INTERNAL | true | [gitlab_host, relative_url_root, subject.avatar.url]
- true | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url]
- true | Project::PUBLIC | true | [subject.avatar.url]
- true | Project::PUBLIC | false | [asset_host, subject.avatar.url]
- false | Project::PRIVATE | true | [relative_url_root, subject.avatar.url]
- false | Project::PRIVATE | false | [gitlab_host, relative_url_root, subject.avatar.url]
- false | Project::INTERNAL | true | [relative_url_root, subject.avatar.url]
- false | Project::INTERNAL | false | [gitlab_host, relative_url_root, subject.avatar.url]
- false | Project::PUBLIC | true | [relative_url_root, subject.avatar.url]
- false | Project::PUBLIC | false | [gitlab_host, relative_url_root, subject.avatar.url]
+ where(:has_asset_host, :visibility_level, :only_path, :avatar_path_prefix) do
+ true | Project::PRIVATE | true | [gitlab_host, relative_url_root]
+ true | Project::PRIVATE | false | [gitlab_host, relative_url_root]
+ true | Project::INTERNAL | true | [gitlab_host, relative_url_root]
+ true | Project::INTERNAL | false | [gitlab_host, relative_url_root]
+ true | Project::PUBLIC | true | []
+ true | Project::PUBLIC | false | [asset_host]
+ false | Project::PRIVATE | true | [relative_url_root]
+ false | Project::PRIVATE | false | [gitlab_host, relative_url_root]
+ false | Project::INTERNAL | true | [relative_url_root]
+ false | Project::INTERNAL | false | [gitlab_host, relative_url_root]
+ false | Project::PUBLIC | true | [relative_url_root]
+ false | Project::PUBLIC | false | [gitlab_host, relative_url_root]
end
with_them do
before do
- allow(ActionController::Base).to receive(:asset_host).and_return(has_asset_host ? asset_host : nil)
- subject.visibility_level = visibility_level
+ allow(ActionController::Base).to receive(:asset_host) { has_asset_host && asset_host }
+
+ project.visibility_level = visibility_level
end
+ let(:avatar_path) { (avatar_path_prefix + [project.avatar.url]).join }
+
it 'returns the expected avatar path' do
- expect(subject.avatar_path(only_path: only_path)).to eq(avatar_path.join)
+ expect(project.avatar_path(only_path: only_path)).to eq(avatar_path)
end
end
end
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 2322eb206fb..30572ce9332 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -20,6 +20,16 @@ describe DiscussionOnDiff do
expect(truncated_lines).not_to include(be_meta)
end
end
+
+ context "when the diff line does not exist on a legacy diff note" do
+ it "returns an empty array" do
+ legacy_note = LegacyDiffNote.new
+
+ allow(subject).to receive(:first_note).and_return(legacy_note)
+
+ expect(truncated_lines).to eq([])
+ end
+ end
end
describe '#line_code_in_diffs' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 5e82a2988ce..5ea4acb6687 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -582,4 +582,20 @@ describe Group do
end
end
end
+
+ describe '#has_parent?' do
+ context 'when the group has a parent' do
+ it 'should be 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
+ group = create(:group, parent: nil)
+ expect(group.has_parent?).to be_falsy
+ end
+ end
+ end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 4cd9e3f4f1d..bf5703ac986 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -1,13 +1,6 @@
require 'spec_helper'
describe Key, :mailer do
- include Gitlab::CurrentSettings
-
- describe 'modules' do
- subject { described_class }
- it { is_expected.to include_module(Gitlab::CurrentSettings) }
- end
-
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
@@ -79,16 +72,53 @@ describe Key, :mailer do
expect(build(:key)).to be_valid
end
- it 'accepts a key with newline charecters after stripping them' do
- key = build(:key)
- key.key = key.key.insert(100, "\n")
- key.key = key.key.insert(40, "\r\n")
- expect(key).to be_valid
- end
-
it 'rejects the unfingerprintable key (not a key)' do
expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid
end
+
+ where(:factory, :chars, :expected_sections) do
+ [
+ [:key, ["\n", "\r\n"], 3],
+ [:key, [' ', ' '], 3],
+ [:key_without_comment, [' ', ' '], 2]
+ ]
+ end
+
+ with_them do
+ let!(:key) { create(factory) }
+ let!(:original_fingerprint) { key.fingerprint }
+
+ it 'accepts a key with blank space characters after stripping them' do
+ modified_key = key.key.insert(100, chars.first).insert(40, chars.last)
+ _, content = modified_key.split
+
+ key.update!(key: modified_key)
+
+ expect(key).to be_valid
+ expect(key.key.split.size).to eq(expected_sections)
+
+ expect(content).not_to match(/\s/)
+ expect(original_fingerprint).to eq(key.fingerprint)
+ end
+ end
+ end
+
+ context 'validate size' do
+ where(:key_content, :result) do
+ [
+ [Spec::Support::Helpers::KeyGeneratorHelper.new(512).generate, false],
+ [Spec::Support::Helpers::KeyGeneratorHelper.new(8192).generate, false],
+ [Spec::Support::Helpers::KeyGeneratorHelper.new(1024).generate, true]
+ ]
+ end
+
+ with_them do
+ it 'validates the size of the key' do
+ key = build(:key, key: key_content)
+
+ expect(key.valid?).to eq(result)
+ end
+ end
end
context 'validate it meets key restrictions' do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 6aa0e7f49c3..c64cdf8f812 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -488,7 +488,7 @@ describe Member do
member.accept_invite!(user)
end
- it "refreshes user's authorized projects", :truncate do
+ it "refreshes user's authorized projects", :delete do
project = member.source
expect(user.authorized_projects).not_to include(project)
@@ -523,7 +523,7 @@ describe Member do
end
end
- describe "destroying a record", :truncate do
+ describe "destroying a record", :delete do
it "refreshes user's authorized projects" do
project = create(:project, :private)
user = create(:user)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index c76f32b3989..243eeddc7a8 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1064,16 +1064,6 @@ describe MergeRequest do
end
describe '#can_be_reverted?' do
- context 'when there is no merged_at for the MR' do
- before do
- subject.metrics.update!(merged_at: nil)
- end
-
- it 'returns false' do
- expect(subject.can_be_reverted?(nil)).to be_falsey
- end
- end
-
context 'when there is no merge_commit for the MR' do
before do
subject.metrics.update!(merged_at: Time.now.utc)
@@ -1097,6 +1087,16 @@ describe MergeRequest do
end
end
+ context 'when there is no merged_at for the MR' do
+ before do
+ subject.metrics.update!(merged_at: nil)
+ end
+
+ it 'returns true' do
+ expect(subject.can_be_reverted?(nil)).to be_truthy
+ end
+ end
+
context 'when there is a revert commit' do
let(:current_user) { subject.author }
let(:branch) { subject.target_branch }
@@ -1127,9 +1127,29 @@ describe MergeRequest do
end
end
- context 'when the revert commit is mentioned in a note before the MR was merged' do
+ context 'when there is no merged_at for the MR' do
+ before do
+ subject.metrics.update!(merged_at: nil)
+ end
+
+ it 'returns false' do
+ expect(subject.can_be_reverted?(current_user)).to be_falsey
+ end
+ end
+
+ context 'when the revert commit is mentioned in a note just before the MR was merged' do
before do
- subject.notes.last.update!(created_at: subject.metrics.merged_at - 1.second)
+ subject.notes.last.update!(created_at: subject.metrics.merged_at - 30.seconds)
+ end
+
+ it 'returns false' do
+ expect(subject.can_be_reverted?(current_user)).to be_falsey
+ end
+ end
+
+ context 'when the revert commit is mentioned in a note long before the MR was merged' do
+ before do
+ subject.notes.last.update!(created_at: subject.metrics.merged_at - 2.minutes)
end
it 'returns true' do
@@ -1319,6 +1339,10 @@ describe MergeRequest do
it 'returns false' do
expect(subject.mergeable_state?).to be_falsey
end
+
+ it 'returns true when skipping discussions check' do
+ expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true)
+ end
end
end
end
@@ -1519,7 +1543,7 @@ describe MergeRequest do
expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
end
- it "executs diff cache service" do
+ it "executes diff cache service" do
expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
subject.reload_diff
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index c3673a0e2a3..5e126bc4bea 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -204,7 +204,7 @@ describe Namespace do
let(:parent) { create(:group, name: 'parent', path: 'parent') }
let(:child) { create(:group, name: 'child', path: 'child', parent: parent) }
let!(:project) { create(:project_empty_repo, path: 'the-project', namespace: child, skip_disk_validation: true) }
- let(:uploads_dir) { File.join(CarrierWave.root, FileUploader.base_dir) }
+ let(:uploads_dir) { FileUploader.root }
let(:pages_dir) { File.join(TestEnv.pages_path) }
before do
@@ -567,32 +567,62 @@ describe Namespace do
end
end
- describe "#allowed_path_by_redirects" do
- let(:namespace1) { create(:namespace, path: 'foo') }
+ describe '#remove_exports' do
+ let(:legacy_project) { create(:project, :with_export, namespace: namespace) }
+ let(:hashed_project) { create(:project, :with_export, :hashed, namespace: namespace) }
+ let(:export_path) { Dir.mktmpdir('namespace_remove_exports_spec') }
+ let(:legacy_export) { legacy_project.export_project_path }
+ let(:hashed_export) { hashed_project.export_project_path }
- context "when the path has been taken before" do
- before do
- namespace1.path = 'bar'
- namespace1.save!
+ it 'removes exports for legacy and hashed projects' do
+ allow(Gitlab::ImportExport).to receive(:storage_path) { export_path }
+
+ expect(File.exist?(legacy_export)).to be_truthy
+ expect(File.exist?(hashed_export)).to be_truthy
+
+ namespace.remove_exports!
+
+ expect(File.exist?(legacy_export)).to be_falsy
+ expect(File.exist?(hashed_export)).to be_falsy
+ end
+ end
+
+ describe '#full_path_was' 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)
end
+ end
+
+ context 'when a parent is assigned to a group with no previous parent' do
+ it 'should return the path was' do
+ group = create(:group, parent: nil)
- it 'should be invalid' do
- namespace2 = build(:group, path: 'foo')
- expect(namespace2).to be_invalid
+ parent = create(:group)
+ group.parent = parent
+
+ expect(group.full_path_was).to eq("#{group.path_was}")
end
+ end
+
+ context 'when a parent is removed from the group' do
+ it 'should return the parent full path' do
+ parent = create(:group)
+ group = create(:group, parent: parent)
+ group.parent = nil
- it 'should return an error on path' do
- namespace2 = build(:group, path: 'foo')
- namespace2.valid?
- expect(namespace2.errors.messages[:path].first).to eq('foo has been taken before. Please use another one')
+ expect(group.full_path_was).to eq("#{parent.full_path}/#{group.path}")
end
end
- context "when the path has not been taken before" do
- it 'should be valid' do
- expect(RedirectRoute.count).to eq(0)
- namespace = build(:namespace)
- expect(namespace).to be_valid
+ context 'when changing parents' do
+ it 'should return 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}")
end
end
end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 3d030927036..c853f707e6d 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -8,7 +8,7 @@ describe Note do
it { is_expected.to belong_to(:noteable).touch(false) }
it { is_expected.to belong_to(:author).class_name('User') }
- it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:todos) }
end
describe 'modules' do
@@ -17,8 +17,6 @@ describe Note do
it { is_expected.to include_module(Participable) }
it { is_expected.to include_module(Mentionable) }
it { is_expected.to include_module(Awardable) }
-
- it { is_expected.to include_module(Gitlab::CurrentSettings) }
end
describe 'validation' do
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 41e2ab20d69..1fccf92627a 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -30,7 +30,7 @@ describe ProjectGroupLink do
end
end
- describe "destroying a record", :truncate do
+ describe "destroying a record", :delete do
it "refreshes group users' authorized projects" do
project = create(:project, :private)
group = create(:group)
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index c9b3c6cf602..748c366efca 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -3,6 +3,29 @@ require 'spec_helper'
describe JiraService do
include Gitlab::Routing
+ describe '#options' do
+ let(:service) do
+ described_class.new(
+ project: build_stubbed(:project),
+ active: true,
+ username: 'username',
+ password: 'test',
+ jira_issue_transition_id: 24,
+ url: 'http://jira.test.com/path/'
+ )
+ end
+
+ it 'sets the URL properly' do
+ # jira-ruby gem parses the URI and handles trailing slashes
+ # fine: https://github.com/sumoheavy/jira-ruby/blob/v1.4.1/lib/jira/http_client.rb#L59
+ expect(service.options[:site]).to eq('http://jira.test.com/')
+ end
+
+ it 'leaves out trailing slashes in context' do
+ expect(service.options[:context_path]).to eq('/path')
+ end
+ end
+
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -182,7 +205,7 @@ describe JiraService do
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
- body: /#{custom_base_url}\/#{project.full_path}\/commit\/#{merge_request.diff_head_sha}/
+ body: %r{#{custom_base_url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}}
).once
end
@@ -197,7 +220,7 @@ describe JiraService do
@jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
expect(WebMock).to have_requested(:post, @comment_url).with(
- body: /#{Gitlab.config.gitlab.url}\/#{project.full_path}\/commit\/#{merge_request.diff_head_sha}/
+ body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}}
).once
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4d10df410ab..da940571bc1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -117,7 +117,6 @@ describe Project do
it { is_expected.to include_module(Gitlab::ConfigHelper) }
it { is_expected.to include_module(Gitlab::ShellAdapter) }
it { is_expected.to include_module(Gitlab::VisibilityLevel) }
- it { is_expected.to include_module(Gitlab::CurrentSettings) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
end
@@ -2504,6 +2503,37 @@ describe Project do
end
end
+ describe '#remove_exports' do
+ let(:project) { create(:project, :with_export) }
+
+ it 'removes the exports directory for the project' do
+ expect(File.exist?(project.export_path)).to be_truthy
+
+ allow(FileUtils).to receive(:rm_rf).and_call_original
+ expect(FileUtils).to receive(:rm_rf).with(project.export_path).and_call_original
+ project.remove_exports
+
+ expect(File.exist?(project.export_path)).to be_falsy
+ end
+
+ it 'is a no-op when there is no namespace' do
+ export_path = project.export_path
+ project.update_column(:namespace_id, nil)
+
+ expect(FileUtils).not_to receive(:rm_rf).with(export_path)
+
+ project.remove_exports
+
+ expect(File.exist?(export_path)).to be_truthy
+ end
+
+ it 'is run when the project is destroyed' do
+ expect(project).to receive(:remove_exports).and_call_original
+
+ project.destroy
+ end
+ end
+
describe '#forks_count' do
it 'returns the number of forks' do
project = build(:project)
@@ -3228,5 +3258,22 @@ describe Project do
project = build(:project)
project.execute_hooks({ data: 'data' }, :merge_request_hooks)
end
+
+ it 'executes the system hooks when inside a transaction' do
+ allow_any_instance_of(WebHookService).to receive(:execute)
+
+ create(:system_hook, merge_requests_events: true)
+
+ project = build(:project)
+
+ # Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1,
+ # but since the entire spec run takes place in a transaction, we never
+ # actually get to the `after_commit` hook that queues these jobs.
+ expect do
+ project.transaction do
+ project.execute_hooks({ data: 'data' }, :merge_request_hooks)
+ end
+ end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError
+ end
end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 929086305ba..1e7671476f1 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -127,7 +127,7 @@ describe ProjectWiki do
end
after do
- destroy_page(subject.pages.first.page)
+ subject.pages.each { |page| destroy_page(page.page) }
end
it "returns the latest version of the page if it exists" do
@@ -148,6 +148,17 @@ describe ProjectWiki do
page = subject.find_page("index page")
expect(page).to be_a WikiPage
end
+
+ context 'pages with multibyte-character title' do
+ before do
+ create_page("autre pagé", "C'est un génial Gollum Wiki")
+ end
+
+ it "can find a page by slug" do
+ page = subject.find_page("autre pagé")
+ expect(page.title).to eq("autre pagé")
+ end
+ end
end
context 'when Gitaly wiki_find_page is enabled' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 8f406253f39..02a5ee54262 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -36,26 +36,49 @@ describe Repository do
end
describe '#branch_names_contains' do
- subject { repository.branch_names_contains(sample_commit.id) }
+ shared_examples '#branch_names_contains' do
+ set(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
- it { is_expected.to include('master') }
- it { is_expected.not_to include('feature') }
- it { is_expected.not_to include('fix') }
+ subject { repository.branch_names_contains(sample_commit.id) }
- describe 'when storage is broken', :broken_storage do
- it 'should raise a storage error' do
- expect_to_raise_storage_error do
- broken_repository.branch_names_contains(sample_commit.id)
+ it { is_expected.to include('master') }
+ it { is_expected.not_to include('feature') }
+ it { is_expected.not_to include('fix') }
+
+ describe 'when storage is broken', :broken_storage do
+ it 'should raise a storage error' do
+ expect_to_raise_storage_error do
+ broken_repository.branch_names_contains(sample_commit.id)
+ end
end
end
end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like '#branch_names_contains'
+ end
+
+ context 'when gitaly is disabled', :skip_gitaly_mock do
+ it_behaves_like '#branch_names_contains'
+ end
end
describe '#tag_names_contains' do
- subject { repository.tag_names_contains(sample_commit.id) }
+ shared_examples '#tag_names_contains' do
+ subject { repository.tag_names_contains(sample_commit.id) }
+
+ it { is_expected.to include('v1.1.0') }
+ it { is_expected.not_to include('v1.0.0') }
+ end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like '#tag_names_contains'
+ end
- it { is_expected.to include('v1.1.0') }
- it { is_expected.not_to include('v1.0.0') }
+ context 'when gitaly is enabled', :skip_gitaly_mock do
+ it_behaves_like '#tag_names_contains'
+ end
end
describe 'tags_sorted_by' do
@@ -222,20 +245,20 @@ describe Repository do
it 'sets follow when path is a single path' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice
- repository.commits('master', path: 'README.md')
- repository.commits('master', path: ['README.md'])
+ repository.commits('master', limit: 1, path: 'README.md')
+ repository.commits('master', limit: 1, path: ['README.md'])
end
it 'does not set follow when path is multiple paths' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
- repository.commits('master', path: ['README.md', 'CHANGELOG'])
+ repository.commits('master', limit: 1, path: ['README.md', 'CHANGELOG'])
end
it 'does not set follow when there are no paths' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
- repository.commits('master')
+ repository.commits('master', limit: 1)
end
end
@@ -455,7 +478,7 @@ describe Repository do
expect do
repository.create_dir(user, 'newdir',
message: 'Create newdir', branch_name: 'master')
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
newdir = repository.tree('master', 'newdir')
expect(newdir.path).to eq('newdir')
@@ -469,7 +492,7 @@ describe Repository do
repository.create_dir(user, 'newdir',
message: 'Create newdir', branch_name: 'patch',
start_branch_name: 'master', start_project: forked_project)
- end.to change { repository.commits('master').count }.by(0)
+ end.to change { repository.count_commits(ref: 'master') }.by(0)
expect(repository.branch_exists?('patch')).to be_truthy
expect(forked_project.repository.branch_exists?('patch')).to be_falsy
@@ -486,7 +509,7 @@ describe Repository do
message: 'Add newdir',
branch_name: 'master',
author_email: author_email, author_name: author_name)
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
last_commit = repository.commit
@@ -502,7 +525,7 @@ describe Repository do
repository.create_file(user, 'NEWCHANGELOG', 'Changelog!',
message: 'Create changelog',
branch_name: 'master')
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
blob = repository.blob_at('master', 'NEWCHANGELOG')
@@ -514,7 +537,7 @@ describe Repository do
repository.create_file(user, 'new_dir/new_file.txt', 'File!',
message: 'Create new_file with new_dir',
branch_name: 'master')
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
expect(repository.tree('master', 'new_dir').path).to eq('new_dir')
expect(repository.blob_at('master', 'new_dir/new_file.txt').data).to eq('File!')
@@ -538,7 +561,7 @@ describe Repository do
branch_name: 'master',
author_email: author_email,
author_name: author_name)
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
last_commit = repository.commit
@@ -554,7 +577,7 @@ describe Repository do
repository.update_file(user, 'CHANGELOG', 'Changelog!',
message: 'Update changelog',
branch_name: 'master')
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
blob = repository.blob_at('master', 'CHANGELOG')
@@ -567,7 +590,7 @@ describe Repository do
branch_name: 'master',
previous_path: 'LICENSE',
message: 'Changes filename')
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
files = repository.ls_files('master')
@@ -584,7 +607,7 @@ describe Repository do
message: 'Update README',
author_email: author_email,
author_name: author_name)
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
last_commit = repository.commit
@@ -599,7 +622,7 @@ describe Repository do
expect do
repository.delete_file(user, 'README',
message: 'Remove README', branch_name: 'master')
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
expect(repository.blob_at('master', 'README')).to be_nil
end
@@ -610,7 +633,7 @@ describe Repository do
repository.delete_file(user, 'README',
message: 'Remove README', branch_name: 'master',
author_email: author_email, author_name: author_name)
- end.to change { repository.commits('master').count }.by(1)
+ end.to change { repository.count_commits(ref: 'master') }.by(1)
last_commit = repository.commit
@@ -772,8 +795,7 @@ describe Repository do
user, 'LICENSE', 'Copyright!',
message: 'Add LICENSE', branch_name: 'master')
- allow(repository).to receive(:file_on_head)
- .and_raise(Rugged::ReferenceError)
+ allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository)
expect(repository.license_blob).to be_nil
end
@@ -885,7 +907,7 @@ describe Repository do
end
it 'returns nil for empty repository' do
- allow(repository).to receive(:file_on_head).and_raise(Rugged::ReferenceError)
+ allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository)
expect(repository.gitlab_ci_yml).to be_nil
end
end
@@ -960,19 +982,19 @@ describe Repository do
end
describe '#find_branch' do
- it 'loads a branch with a fresh repo' do
- expect(Gitlab::Git::Repository).to receive(:new).twice.and_call_original
+ context 'fresh_repo is true' do
+ it 'delegates the call to raw_repository' do
+ expect(repository.raw_repository).to receive(:find_branch).with('master', true)
- 2.times do
- expect(repository.find_branch('feature')).not_to be_nil
+ repository.find_branch('master', fresh_repo: true)
end
end
- it 'loads a branch with a cached repo' do
- expect(Gitlab::Git::Repository).to receive(:new).once.and_call_original
+ context 'fresh_repo is false' do
+ it 'delegates the call to raw_repository' do
+ expect(repository.raw_repository).to receive(:find_branch).with('master', false)
- 2.times do
- expect(repository.find_branch('feature', fresh_repo: false)).not_to be_nil
+ repository.find_branch('master', fresh_repo: false)
end
end
end
@@ -1937,8 +1959,7 @@ describe Repository do
describe '#avatar' do
it 'returns nil if repo does not exist' do
- expect(repository).to receive(:file_on_head)
- .and_raise(Rugged::ReferenceError)
+ allow(repository).to receive(:root_ref).and_raise(Gitlab::Git::Repository::NoRepository)
expect(repository.avatar).to eq(nil)
end
@@ -2356,7 +2377,7 @@ describe Repository do
let(:commit) { repository.commit }
let(:ancestor) { commit.parents.first }
- context 'with Gitaly enabled' do
+ shared_examples '#ancestor?' do
it 'it is an ancestor' do
expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
end
@@ -2370,27 +2391,19 @@ describe Repository do
expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
expect(repository.ancestor?(nil, nil)).to eq(false)
end
- end
-
- context 'with Gitaly disabled' do
- before do
- allow(Gitlab::GitalyClient).to receive(:enabled?).and_return(false)
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(false)
- end
- it 'it is an ancestor' do
- expect(repository.ancestor?(ancestor.id, commit.id)).to eq(true)
+ it 'returns false for invalid commit IDs' do
+ expect(repository.ancestor?(commit.id, Gitlab::Git::BLANK_SHA)).to eq(false)
+ expect(repository.ancestor?( Gitlab::Git::BLANK_SHA, commit.id)).to eq(false)
end
+ end
- it 'it is not an ancestor' do
- expect(repository.ancestor?(commit.id, ancestor.id)).to eq(false)
- end
+ context 'with Gitaly enabled' do
+ it_behaves_like('#ancestor?')
+ end
- it 'returns false on nil-values' do
- expect(repository.ancestor?(nil, commit.id)).to eq(false)
- expect(repository.ancestor?(ancestor.id, nil)).to eq(false)
- expect(repository.ancestor?(nil, nil)).to eq(false)
- end
+ context 'with Gitaly disabled', :skip_gitaly_mock do
+ it_behaves_like('#ancestor?')
end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index 3e8f3848eca..bd498269798 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -20,6 +20,7 @@ describe Todo do
it { is_expected.to validate_presence_of(:action) }
it { is_expected.to validate_presence_of(:target_type) }
it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:author) }
context 'for commits' do
subject { described_class.new(target_type: 'Commit') }
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 345382ea8c7..0dcaa026332 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -45,51 +45,6 @@ describe Upload do
end
end
- describe '.remove_path' do
- it 'removes all records at the given path' do
- described_class.create!(
- size: File.size(__FILE__),
- path: __FILE__,
- model: build_stubbed(:user),
- uploader: 'AvatarUploader'
- )
-
- expect { described_class.remove_path(__FILE__) }
- .to change { described_class.count }.from(1).to(0)
- end
- end
-
- describe '.record' do
- let(:fake_uploader) do
- double(
- file: double(size: 12_345),
- relative_path: 'foo/bar.jpg',
- model: build_stubbed(:user),
- class: 'AvatarUploader'
- )
- end
-
- it 'removes existing paths before creation' do
- expect(described_class).to receive(:remove_path)
- .with(fake_uploader.relative_path)
-
- described_class.record(fake_uploader)
- end
-
- it 'creates a new record and assigns size, path, model, and uploader' do
- upload = described_class.record(fake_uploader)
-
- aggregate_failures do
- expect(upload).to be_persisted
- expect(upload.size).to eq fake_uploader.file.size
- expect(upload.path).to eq fake_uploader.relative_path
- expect(upload.model_id).to eq fake_uploader.model.id
- expect(upload.model_type).to eq fake_uploader.model.class.to_s
- expect(upload.uploader).to eq fake_uploader.class
- end
- end
- end
-
describe '#absolute_path' do
it 'returns the path directly when already absolute' do
path = '/path/to/namespace/project/secret/file.jpg'
@@ -111,27 +66,27 @@ describe Upload do
end
end
- describe '#calculate_checksum' do
- it 'calculates the SHA256 sum' do
- upload = described_class.new(
- path: __FILE__,
- size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
- )
+ describe '#calculate_checksum!' do
+ let(:upload) do
+ described_class.new(path: __FILE__,
+ size: described_class::CHECKSUM_THRESHOLD - 1.megabyte)
+ end
+
+ it 'sets `checksum` to SHA256 sum of the file' do
expected = Digest::SHA256.file(__FILE__).hexdigest
- expect { upload.calculate_checksum }
+ expect { upload.calculate_checksum! }
.to change { upload.checksum }.from(nil).to(expected)
end
- it 'returns nil for a non-existant file' do
- upload = described_class.new(
- path: __FILE__,
- size: described_class::CHECKSUM_THRESHOLD - 1.megabyte
- )
-
+ it 'sets `checksum` to nil for a non-existant file' do
expect(upload).to receive(:exist?).and_return(false)
- expect(upload.calculate_checksum).to be_nil
+ checksum = Digest::SHA256.file(__FILE__).hexdigest
+ upload.checksum = checksum
+
+ expect { upload.calculate_checksum! }
+ .to change { upload.checksum }.from(checksum).to(nil)
end
end
@@ -148,4 +103,10 @@ describe Upload do
expect(upload).not_to exist
end
end
+
+ describe "#uploader_context" do
+ subject { create(:upload, :issuable_upload, secret: 'secret', filename: 'file.txt') }
+
+ it { expect(subject.uploader_context).to match(a_hash_including(secret: 'secret', identifier: 'file.txt')) }
+ end
end
diff --git a/spec/models/user_callout_spec.rb b/spec/models/user_callout_spec.rb
new file mode 100644
index 00000000000..64ba17c81fe
--- /dev/null
+++ b/spec/models/user_callout_spec.rb
@@ -0,0 +1,16 @@
+require 'rails_helper'
+
+describe UserCallout do
+ let!(:callout) { create(:user_callout) }
+
+ describe 'relationships' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:user) }
+
+ it { is_expected.to validate_presence_of(:feature_name) }
+ it { is_expected.to validate_uniqueness_of(:feature_name).scoped_to(:user_id).ignoring_case_sensitivity }
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 762cec9b95e..568aab8530e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,14 +1,12 @@
require 'spec_helper'
describe User do
- include Gitlab::CurrentSettings
include ProjectForksHelper
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Gitlab::ConfigHelper) }
- it { is_expected.to include_module(Gitlab::CurrentSettings) }
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(TokenAuthenticatable) }
@@ -35,7 +33,7 @@ describe User do
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
- it { is_expected.to have_many(:todos).dependent(:destroy) }
+ it { is_expected.to have_many(:todos) }
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
it { is_expected.to have_many(:triggers).dependent(:destroy) }
it { is_expected.to have_many(:builds).dependent(:nullify) }
@@ -560,7 +558,7 @@ describe User do
stub_config_setting(default_can_create_group: true)
expect { user.update_attributes(external: false) }.to change { user.can_create_group }.to(true)
- .and change { user.projects_limit }.to(current_application_settings.default_projects_limit)
+ .and change { user.projects_limit }.to(Gitlab::CurrentSettings.default_projects_limit)
end
end
@@ -826,7 +824,7 @@ describe User do
end
end
- context 'when current_application_settings.user_default_external is true' do
+ context 'when Gitlab::CurrentSettings.user_default_external is true' do
before do
stub_application_setting(user_default_external: true)
end
@@ -1569,7 +1567,7 @@ describe User do
it { is_expected.to eq([private_group]) }
end
- describe '#authorized_projects', :truncate do
+ describe '#authorized_projects', :delete do
context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do
user = create(:user)
@@ -2619,7 +2617,7 @@ describe User do
it 'should raise an ActiveRecord::RecordInvalid exception' do
user2 = build(:user, username: 'foo')
- expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Path foo has been taken before/)
+ expect { user2.save! }.to raise_error(ActiveRecord::RecordInvalid, /Route path foo has been taken before. Please use another one, Route is invalid/)
end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index ea75434e399..4a1d12cd448 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -188,14 +188,37 @@ describe WikiPage do
end
end
- describe "#update" do
+ describe '#create', :skip_gitaly_mock do
+ context 'with valid attributes' do
+ it 'raises an error if a page with the same path already exists' do
+ create_page('New Page', 'content')
+ create_page('foo/bar', 'content')
+ expect { create_page('New Page', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
+ expect { create_page('foo/bar', 'other content') }.to raise_error Gitlab::Git::Wiki::DuplicatePageError
+
+ destroy_page('New Page')
+ destroy_page('bar', 'foo')
+ end
+
+ it 'if the title is preceded by a / it is removed' do
+ create_page('/New Page', 'content')
+
+ expect(wiki.find_page('New Page')).not_to be_nil
+
+ destroy_page('New Page')
+ end
+ end
+ end
+
+ # Remove skip_gitaly_mock flag when gitaly_update_page implements moving pages
+ describe "#update", :skip_gitaly_mock do
before do
create_page("Update", "content")
@page = wiki.find_page("Update")
end
after do
- destroy_page(@page.title)
+ destroy_page(@page.title, @page.directory)
end
context "with valid attributes" do
@@ -233,6 +256,95 @@ describe WikiPage do
expect { @page.update(content: 'more content', last_commit_sha: 'xxx') }.to raise_error(WikiPage::PageChangedError)
end
end
+
+ context 'when renaming a page' do
+ it 'raises an error if the page already exists' do
+ create_page('Existing Page', 'content')
+
+ expect { @page.update(title: 'Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
+ expect(@page.title).to eq 'Update'
+ expect(@page.content).to eq 'new_content'
+
+ destroy_page('Existing Page')
+ end
+
+ it 'updates the content and rename the file' do
+ new_title = 'Renamed Page'
+ new_content = 'updated content'
+
+ expect(@page.update(title: new_title, content: new_content)).to be_truthy
+
+ @page = wiki.find_page(new_title)
+
+ expect(@page).not_to be_nil
+ expect(@page.content).to eq new_content
+ end
+ end
+
+ context 'when moving a page' do
+ it 'raises an error if the page already exists' do
+ create_page('foo/Existing Page', 'content')
+
+ expect { @page.update(title: 'foo/Existing Page', content: 'new_content') }.to raise_error(WikiPage::PageRenameError)
+ expect(@page.title).to eq 'Update'
+ expect(@page.content).to eq 'new_content'
+
+ destroy_page('Existing Page', 'foo')
+ end
+
+ it 'updates the content and moves the file' do
+ new_title = 'foo/Other Page'
+ new_content = 'new_content'
+
+ expect(@page.update(title: new_title, content: new_content)).to be_truthy
+
+ page = wiki.find_page(new_title)
+
+ expect(page).not_to be_nil
+ expect(page.content).to eq new_content
+ end
+
+ context 'in subdir' do
+ before do
+ create_page('foo/Existing Page', 'content')
+ @page = wiki.find_page('foo/Existing Page')
+ end
+
+ it 'moves the page to the root folder if the title is preceded by /' do
+ expect(@page.slug).to eq 'foo/Existing-Page'
+ expect(@page.update(title: '/Existing Page', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq 'Existing-Page'
+ end
+
+ it 'does nothing if it has the same title' do
+ original_path = @page.slug
+
+ expect(@page.update(title: 'Existing Page', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq original_path
+ end
+ end
+
+ context 'in root dir' do
+ it 'does nothing if the title is preceded by /' do
+ original_path = @page.slug
+
+ expect(@page.update(title: '/Update', content: 'new_content')).to be_truthy
+ expect(@page.slug).to eq original_path
+ end
+ end
+ end
+
+ context "with invalid attributes" do
+ it 'aborts update if title blank' do
+ expect(@page.update(title: '', content: 'new_content')).to be_falsey
+ expect(@page.content).to eq 'new_content'
+
+ page = wiki.find_page('Update')
+ expect(page.content).to eq 'content'
+
+ @page.title = 'Update'
+ end
+ end
end
describe "#destroy" do
@@ -386,6 +498,27 @@ describe WikiPage do
end
end
+ describe '#formatted_content' do
+ shared_examples 'fetching page formatted content' do
+ it 'returns processed content of the page' do
+ subject.create({ title: "RDoc", content: "*bold*", format: "rdoc" })
+ page = wiki.find_page('RDoc')
+
+ expect(page.formatted_content).to eq("\n<p><strong>bold</strong></p>\n")
+
+ destroy_page('RDoc')
+ end
+ end
+
+ context 'when Gitaly wiki_page_formatted_data is enabled' do
+ it_behaves_like 'fetching page formatted content'
+ end
+
+ context 'when Gitaly wiki_page_formatted_data is disabled', :disable_gitaly do
+ it_behaves_like 'fetching page formatted content'
+ end
+ end
+
private
def remove_temp_repo(path)
@@ -400,8 +533,8 @@ describe WikiPage do
wiki.wiki.write_page(name, :markdown, content, commit_details)
end
- def destroy_page(title)
- page = wiki.wiki.page(title: title)
+ def destroy_page(title, dir = '')
+ page = wiki.wiki.page(title: title, dir: dir)
wiki.delete_page(page, "test commit")
end
diff --git a/spec/policies/ci/pipeline_schedule_policy_spec.rb b/spec/policies/ci/pipeline_schedule_policy_spec.rb
index 1b0e9fac355..c0c3eda4911 100644
--- a/spec/policies/ci/pipeline_schedule_policy_spec.rb
+++ b/spec/policies/ci/pipeline_schedule_policy_spec.rb
@@ -88,5 +88,19 @@ describe Ci::PipelineSchedulePolicy, :models do
expect(policy).to be_allowed :admin_pipeline_schedule
end
end
+
+ describe 'rules for non-owner of schedule' do
+ let(:owner) { create(:user) }
+
+ before do
+ project.add_master(owner)
+ project.add_master(user)
+ pipeline_schedule.update(owner: owner)
+ end
+
+ it 'includes abilities to take ownership' do
+ expect(policy).to be_allowed :take_ownership_pipeline_schedule
+ end
+ end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index f2593a1a75c..129344f105f 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -92,7 +92,7 @@ describe ProjectPolicy do
it 'does not include the read_issue permission when the issue author is not a member of the private project' do
project = create(:project, :private)
- issue = create(:issue, project: project)
+ issue = create(:issue, project: project, author: create(:user))
user = issue.author
expect(project.team.member?(issue.author)).to be false
diff --git a/spec/requests/api/applications_spec.rb b/spec/requests/api/applications_spec.rb
new file mode 100644
index 00000000000..f56bc932f40
--- /dev/null
+++ b/spec/requests/api/applications_spec.rb
@@ -0,0 +1,86 @@
+require 'spec_helper'
+
+describe API::Applications, :api do
+ include ApiHelpers
+
+ let(:admin_user) { create(:user, admin: true) }
+ let(:user) { create(:user, admin: false) }
+
+ describe 'POST /applications' do
+ context 'authenticated and authorized user' do
+ it 'creates and returns an OAuth application' do
+ expect do
+ post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url', scopes: ''
+ end.to change { Doorkeeper::Application.count }.by 1
+
+ application = Doorkeeper::Application.find_by(name: 'application_name', redirect_uri: 'http://application.url')
+
+ expect(response).to have_http_status 201
+ expect(json_response).to be_a Hash
+ expect(json_response['application_id']).to eq application.uid
+ expect(json_response['secret']).to eq application.secret
+ expect(json_response['callback_url']).to eq application.redirect_uri
+ end
+
+ it 'does not allow creating an application with the wrong redirect_uri format' do
+ expect do
+ post api('/applications', admin_user), name: 'application_name', redirect_uri: 'wrong_url_format', scopes: ''
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to have_http_status 400
+ expect(json_response).to be_a Hash
+ expect(json_response['message']['redirect_uri'][0]).to eq('must be an absolute URI.')
+ end
+
+ it 'does not allow creating an application without a name' do
+ expect do
+ post api('/applications', admin_user), redirect_uri: 'http://application.url', scopes: ''
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to have_http_status 400
+ expect(json_response).to be_a Hash
+ expect(json_response['error']).to eq('name is missing')
+ end
+
+ it 'does not allow creating an application without a redirect_uri' do
+ expect do
+ post api('/applications', admin_user), name: 'application_name', scopes: ''
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to have_http_status 400
+ expect(json_response).to be_a Hash
+ expect(json_response['error']).to eq('redirect_uri is missing')
+ end
+
+ it 'does not allow creating an application without scopes' do
+ expect do
+ post api('/applications', admin_user), name: 'application_name', redirect_uri: 'http://application.url'
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to have_http_status 400
+ expect(json_response).to be_a Hash
+ expect(json_response['error']).to eq('scopes is missing')
+ end
+ end
+
+ context 'authorized user without authorization' do
+ it 'does not create application' do
+ expect do
+ post api('/applications', user), name: 'application_name', redirect_uri: 'http://application.url', scopes: ''
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to have_http_status 403
+ end
+ end
+
+ context 'non-authenticated user' do
+ it 'does not create application' do
+ expect do
+ post api('/applications'), name: 'application_name', redirect_uri: 'http://application.url'
+ end.not_to change { Doorkeeper::Application.count }
+
+ expect(response).to have_http_status 401
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 34db50dc082..ff5f207487b 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -62,7 +62,7 @@ describe API::Commits do
context "since optional parameter" do
it "returns project commits since provided parameter" do
- commits = project.repository.commits("master")
+ commits = project.repository.commits("master", limit: 2)
after = commits.second.created_at
get api("/projects/#{project_id}/repository/commits?since=#{after.utc.iso8601}", user)
@@ -73,7 +73,7 @@ describe API::Commits do
end
it 'include correct pagination headers' do
- commits = project.repository.commits("master")
+ commits = project.repository.commits("master", limit: 2)
after = commits.second.created_at
commit_count = project.repository.count_commits(ref: 'master', after: after).to_s
@@ -87,12 +87,12 @@ describe API::Commits do
context "until optional parameter" do
it "returns project commits until provided parameter" do
- commits = project.repository.commits("master")
+ commits = project.repository.commits("master", limit: 20)
before = commits.second.created_at
get api("/projects/#{project_id}/repository/commits?until=#{before.utc.iso8601}", user)
- if commits.size >= 20
+ if commits.size == 20
expect(json_response.size).to eq(20)
else
expect(json_response.size).to eq(commits.size - 1)
@@ -103,7 +103,7 @@ describe API::Commits do
end
it 'include correct pagination headers' do
- commits = project.repository.commits("master")
+ commits = project.repository.commits("master", limit: 2)
before = commits.second.created_at
commit_count = project.repository.count_commits(ref: 'master', before: before).to_s
@@ -181,7 +181,7 @@ describe API::Commits do
let(:page) { 3 }
it 'returns the third 5 commits' do
- commit = project.repository.commits('HEAD', offset: (page - 1) * per_page).first
+ commit = project.repository.commits('HEAD', limit: per_page, offset: (page - 1) * per_page).first
expect(json_response.size).to eq(per_page)
expect(json_response.first['id']).to eq(commit.id)
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 3c0b4728dc2..bb0034e3237 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -30,6 +30,21 @@ describe API::Groups do
expect(json_response)
.to satisfy_one { |group| group['name'] == group1.name }
end
+
+ it 'avoids N+1 queries' do
+ # Establish baseline
+ get api("/groups", admin)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/groups", admin)
+ end
+
+ create(:group)
+
+ expect do
+ get api("/groups", admin)
+ end.not_to exceed_query_limit(control)
+ end
end
context "when authenticated as user" do
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 43218755f4f..13db40d21a5 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -1441,7 +1441,7 @@ describe API::Issues, :mailer do
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
- post api("/projects/123/issues/#{issue.iid}/move", user),
+ post api("/projects/12345/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
expect(response).to have_gitlab_http_status(404)
@@ -1452,7 +1452,7 @@ describe API::Issues, :mailer do
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),
- to_project_id: 123
+ to_project_id: 12345
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 4dd8deb6404..f8d0b63afec 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -300,44 +300,53 @@ describe API::Jobs do
end
describe 'GET /projects/:id/jobs/:job_id/artifacts' do
- before do
- get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
+ shared_examples 'downloads artifact' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
+
+ it 'returns specific job artifacts' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(job.artifacts_file.file.file)
+ end
end
- context 'job with artifacts' do
- let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ context 'normal authentication' do
+ context 'job with artifacts' do
+ context 'when artifacts are stored locally' do
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
- context 'authorized user' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
- end
+ before do
+ get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
+ end
- it 'returns specific job artifacts' do
- expect(response).to have_gitlab_http_status(200)
- expect(response.headers).to include(download_headers)
- expect(response.body).to match_file(job.artifacts_file.file.file)
+ context 'authorized user' do
+ it_behaves_like 'downloads artifact'
+ end
+
+ context 'unauthorized user' do
+ let(:api_user) { nil }
+
+ it 'does not return specific job artifacts' do
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
end
- end
- context 'when anonymous user is accessing private artifacts' do
- let(:api_user) { nil }
+ it 'does not return job artifacts if not uploaded' do
+ get api("/projects/#{project.id}/jobs/#{job.id}/artifacts", api_user)
- it 'hides artifacts and rejects request' do
- expect(project).to be_private
expect(response).to have_gitlab_http_status(404)
end
end
end
-
- it 'does not return job artifacts if not uploaded' do
- expect(response).to have_gitlab_http_status(404)
- end
end
describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do
let(:api_user) { reporter }
- let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ let(:job) { create(:ci_build, :artifacts, pipeline: pipeline, user: api_user) }
before do
job.success
@@ -396,14 +405,16 @@ describe API::Jobs do
context 'find proper job' do
shared_examples 'a valid file' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' =>
- "attachment; filename=#{job.artifacts_file.filename}" }
- end
+ context 'when artifacts are stored locally' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' =>
+ "attachment; filename=#{job.artifacts_file.filename}" }
+ end
- it { expect(response).to have_gitlab_http_status(200) }
- it { expect(response.headers).to include(download_headers) }
+ it { expect(response).to have_gitlab_http_status(200) }
+ it { expect(response.headers).to include(download_headers) }
+ end
end
context 'with regular branch' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 73bd4785b11..ec500838eb2 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -44,6 +44,21 @@ describe API::Members do
end
end
+ it 'avoids N+1 queries' do
+ # Establish baseline
+ get api("/#{source_type.pluralize}/#{source.id}/members", master)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/#{source_type.pluralize}/#{source.id}/members", master)
+ end
+
+ project.add_developer(create(:user))
+
+ expect do
+ get api("/#{source_type.pluralize}/#{source.id}/members", master)
+ end.not_to exceed_query_limit(control)
+ end
+
it 'does not return invitees' do
create(:"#{source_type}_member", invite_token: '123', invite_email: 'test@abc.com', source: source, user: nil)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 8e2982f1a5d..14dd9da119d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -198,6 +198,8 @@ describe API::MergeRequests do
create(:merge_request, state: 'closed', milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
+ create(:merge_request, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time)
+
expect do
get api("/projects/#{project.id}/merge_requests", user)
end.not_to exceed_query_limit(control)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 97e7ffcd38e..f11cd638d96 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -2,8 +2,6 @@
require 'spec_helper'
describe API::Projects do
- include Gitlab::CurrentSettings
-
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index cb66d23b77c..c5c0b0c2867 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -945,7 +945,7 @@ describe API::Runner do
context 'when artifacts are being stored inside of tmp path' do
before do
# by configuring this path we allow to pass temp file from any path
- allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
+ allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return('/')
end
context 'when job has been erased' do
@@ -1122,7 +1122,7 @@ describe API::Runner do
# by configuring this path we allow to pass file from @tmpdir only
# but all temporary files are stored in system tmp directory
@tmpdir = Dir.mktmpdir
- allow(JobArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
+ allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return(@tmpdir)
end
after do
diff --git a/spec/requests/api/v3/builds_spec.rb b/spec/requests/api/v3/builds_spec.rb
index af9e36a3b29..3f92288fef0 100644
--- a/spec/requests/api/v3/builds_spec.rb
+++ b/spec/requests/api/v3/builds_spec.rb
@@ -4,16 +4,18 @@ describe API::V3::Builds do
set(:user) { create(:user) }
let(:api_user) { user }
set(:project) { create(:project, :repository, creator: user, public_builds: false) }
- set(:developer) { create(:project_member, :developer, user: user, project: project) }
- set(:reporter) { create(:project_member, :reporter, project: project) }
- set(:guest) { create(:project_member, :guest, project: project) }
- set(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
- let!(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:developer) { create(:project_member, :developer, user: user, project: project) }
+ let(:reporter) { create(:project_member, :reporter, project: project) }
+ let(:guest) { create(:project_member, :guest, project: project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
let(:query) { '' }
before do |example|
+ build
+
create(:ci_build, :skipped, pipeline: pipeline)
unless example.metadata[:skip_before_request]
@@ -110,6 +112,10 @@ describe API::V3::Builds do
end
describe 'GET /projects/:id/repository/commits/:sha/builds' do
+ before do
+ build
+ end
+
context 'when commit does not exist in repository' do
before do
get v3_api("/projects/#{project.id}/repository/commits/1a271fd1/builds", api_user)
@@ -214,18 +220,20 @@ describe API::V3::Builds do
end
context 'job with artifacts' do
- let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
+ context 'when artifacts are stored locally' do
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
- context 'authorized user' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
- end
+ context 'authorized user' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' => 'attachment; filename=ci_build_artifacts.zip' }
+ end
- it 'returns specific job artifacts' do
- expect(response).to have_gitlab_http_status(200)
- expect(response.headers).to include(download_headers)
- expect(response.body).to match_file(build.artifacts_file.file.file)
+ it 'returns specific job artifacts' do
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.headers).to include(download_headers)
+ expect(response.body).to match_file(build.artifacts_file.file.file)
+ end
end
end
@@ -303,14 +311,16 @@ describe API::V3::Builds do
context 'find proper job' do
shared_examples 'a valid file' do
- let(:download_headers) do
- { 'Content-Transfer-Encoding' => 'binary',
- 'Content-Disposition' =>
- "attachment; filename=#{build.artifacts_file.filename}" }
- end
+ context 'when artifacts are stored locally' do
+ let(:download_headers) do
+ { 'Content-Transfer-Encoding' => 'binary',
+ 'Content-Disposition' =>
+ "attachment; filename=#{build.artifacts_file.filename}" }
+ end
- it { expect(response).to have_gitlab_http_status(200) }
- it { expect(response.headers).to include(download_headers) }
+ it { expect(response).to have_gitlab_http_status(200) }
+ it { expect(response.headers).to include(download_headers) }
+ end
end
context 'with regular branch' do
diff --git a/spec/requests/api/v3/commits_spec.rb b/spec/requests/api/v3/commits_spec.rb
index 34c543bffe8..9ef3b859001 100644
--- a/spec/requests/api/v3/commits_spec.rb
+++ b/spec/requests/api/v3/commits_spec.rb
@@ -36,7 +36,7 @@ describe API::V3::Commits do
context "since optional parameter" do
it "returns project commits since provided parameter" do
- commits = project.repository.commits("master")
+ commits = project.repository.commits("master", limit: 2)
since = commits.second.created_at
get v3_api("/projects/#{project.id}/repository/commits?since=#{since.utc.iso8601}", user)
@@ -49,12 +49,12 @@ describe API::V3::Commits do
context "until optional parameter" do
it "returns project commits until provided parameter" do
- commits = project.repository.commits("master")
+ commits = project.repository.commits("master", limit: 20)
before = commits.second.created_at
get v3_api("/projects/#{project.id}/repository/commits?until=#{before.utc.iso8601}", user)
- if commits.size >= 20
+ if commits.size == 20
expect(json_response.size).to eq(20)
else
expect(json_response.size).to eq(commits.size - 1)
diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb
index 13e465e0b2d..5d99d9495f3 100644
--- a/spec/requests/api/v3/projects_spec.rb
+++ b/spec/requests/api/v3/projects_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe API::V3::Projects do
- include Gitlab::CurrentSettings
-
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index bee918a20aa..930ef49b7f3 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -958,7 +958,7 @@ describe 'Git LFS API and storage' do
end
it 'responds with status 200, location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
@@ -1075,7 +1075,7 @@ describe 'Git LFS API and storage' do
end
it 'with location of lfs store and object details' do
- expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['StoreLFSPath']).to eq(LfsObjectUploader.workhorse_upload_path)
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
new file mode 100644
index 00000000000..bcc01b087f3
--- /dev/null
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -0,0 +1,414 @@
+require 'rails_helper'
+
+describe Groups::TransferService, :postgresql do
+ let(:user) { create(:user) }
+ let(:new_parent_group) { create(:group, :public) }
+ let!(:group_member) { create(:group_member, :owner, group: group, user: user) }
+ let(:transfer_service) { described_class.new(group, user) }
+
+ shared_examples 'ensuring allowed transfer for a group' do
+ context 'with other database than PostgreSQL' do
+ before do
+ allow(Group).to receive(:supports_nested_groups?).and_return(false)
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add an error on group' do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq('Transfer failed: Database is not supported.')
+ end
+ end
+
+ context "when there's an exception on Gitlab shell directories" do
+ let(:new_parent_group) { create(:group, :public) }
+
+ before do
+ allow_any_instance_of(described_class).to receive(:update_group_attributes).and_raise(Gitlab::UpdatePathError, 'namespace directory cannot be moved')
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add 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
+ end
+ end
+
+ describe '#execute' do
+ context 'when transforming a group into a root group' do
+ let!(:group) { create(:group, :public, :nested) }
+
+ it_behaves_like 'ensuring allowed transfer for a group'
+
+ context 'when the group is already a root group' do
+ let(:group) { create(:group, :public) }
+
+ it 'should add an error on group' do
+ transfer_service.execute(nil)
+ expect(transfer_service.error).to eq('Transfer failed: Group is already a root group.')
+ end
+ end
+
+ 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
+ expect(transfer_service.execute(nil)).to be_falsy
+ end
+
+ it "should add 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
+ end
+
+ context 'when there is a group with the same path' do
+ let!(:group) { create(:group, :public, :nested, path: 'not-unique') }
+
+ before do
+ create(:group, path: 'not-unique')
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(nil)).to be_falsy
+ end
+
+ it 'should add 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
+ end
+
+ context 'when the group is a subgroup and the transfer is valid' do
+ let!(:subgroup1) { create(:group, :private, parent: group) }
+ let!(:subgroup2) { create(:group, :internal, parent: group) }
+ let!(:project1) { create(:project, :repository, :private, namespace: group) }
+
+ before do
+ transfer_service.execute(nil)
+ group.reload
+ end
+
+ it 'should update group attributes' do
+ expect(group.parent).to be_nil
+ end
+
+ it 'should update 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
+ group.projects.each do |project|
+ expect(project.full_path).to eq("#{group.path}/#{project.path}")
+ end
+ end
+ end
+ end
+
+ context 'when transferring a subgroup into another group' do
+ let(:group) { create(:group, :public, :nested) }
+
+ it_behaves_like 'ensuring allowed transfer for a group'
+
+ 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
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add 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
+ end
+
+ 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
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it "should add 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
+ end
+
+ context 'when the parent has a group with the same path' do
+ before do
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ group.update_attribute(:path, "not-unique")
+ create(:group, path: "not-unique", parent: new_parent_group)
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add 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
+ end
+
+ context 'when the parent group has a project with the same path' do
+ let!(:group) { create(:group, :public, :nested, path: 'foo') }
+
+ before do
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ create(:project, path: 'foo', namespace: new_parent_group)
+ group.update_attribute(:path, 'foo')
+ end
+
+ it 'should return false' do
+ expect(transfer_service.execute(new_parent_group)).to be_falsy
+ end
+
+ it 'should add an error on group' do
+ transfer_service.execute(new_parent_group)
+ expect(transfer_service.error).to eq('Transfer failed: Validation failed: Route path has already been taken, Route is invalid')
+ end
+ end
+
+ context 'when the group is allowed to be transferred' do
+ before do
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ context 'when the group has a lower visibility than the parent group' do
+ let(:new_parent_group) { create(:group, :public) }
+ let(:group) { create(:group, :private, :nested) }
+
+ it 'should 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)
+ end
+ end
+
+ context 'when the group has a higher visibility than the parent group' 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
+ 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
+ expect(group.visibility_level).to eq(new_parent_group.visibility_level)
+ end
+
+ it 'should update 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
+ expect(new_parent_group.children.count).to eq(1)
+ expect(new_parent_group.children.first).to eq(group)
+ end
+
+ it 'should create a permanent redirect for the group' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ end
+ end
+
+ context 'when transferring a group with group descendants' do
+ let!(:subgroup1) { create(:group, :private, parent: group) }
+ let!(:subgroup2) { create(:group, :internal, parent: group) }
+
+ before do
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should update 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 permanent redirects for the subgroups' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup1.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup2.redirect_routes.permanent.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
+ expect(subgroup1.private?).to be_truthy
+ expect(subgroup2.internal?).to be_truthy
+ end
+ end
+
+ context 'when the new parent has a lower visibility than the children' do
+ let!(:subgroup1) { create(:group, :public, parent: group) }
+ 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
+ group.children.each do |subgroup|
+ expect(subgroup.private?).to be_truthy
+ end
+ end
+ end
+ end
+
+ context 'when transferring a group with project descendants' do
+ let!(:project1) { create(:project, :repository, :private, namespace: group) }
+ let!(:project2) { create(:project, :repository, :internal, namespace: group) }
+
+ before do
+ TestEnv.clean_test_path
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should update 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
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ expect(project1.redirect_routes.permanent.count).to eq(1)
+ expect(project2.redirect_routes.permanent.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
+ expect(project1.private?).to be_truthy
+ expect(project2.internal?).to be_truthy
+ end
+ end
+
+ context 'when the new parent has a lower visibility than the projects' do
+ let!(:project1) { create(:project, :repository, :public, namespace: group) }
+ 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
+ group.projects.each do |project|
+ expect(project.private?).to be_truthy
+ end
+ end
+ end
+ end
+
+ context 'when transferring a group with subgroups & projects descendants' do
+ let!(:project1) { create(:project, :repository, :private, namespace: group) }
+ let!(:project2) { create(:project, :repository, :internal, namespace: group) }
+ let!(:subgroup1) { create(:group, :private, parent: group) }
+ let!(:subgroup2) { create(:group, :internal, parent: group) }
+
+ before do
+ TestEnv.clean_test_path
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should update 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
+ 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 redirect for the subgroups and projects' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup1.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup2.redirect_routes.permanent.count).to eq(1)
+ expect(project1.redirect_routes.permanent.count).to eq(1)
+ expect(project2.redirect_routes.permanent.count).to eq(1)
+ end
+ end
+
+ context 'when transfering a group with nested groups and projects' do
+ let!(:group) { create(:group, :public) }
+ let!(:project1) { create(:project, :repository, :private, namespace: group) }
+ let!(:subgroup1) { create(:group, :private, parent: group) }
+ let!(:nested_subgroup) { create(:group, :private, parent: subgroup1) }
+ let!(:nested_project) { create(:project, :repository, :private, namespace: subgroup1) }
+
+ before do
+ TestEnv.clean_test_path
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should update 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}")
+ end
+
+ new_base_path = "#{new_parent_group.path}/#{group.path}/#{subgroup1.path}"
+ subgroup1.children.each do |children|
+ expect(children.full_path).to eq("#{new_base_path}/#{children.path}")
+ end
+ end
+
+ it 'should update 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}"
+ expect(project.full_path).to eq(project_full_path)
+ end
+ end
+
+ it 'should create permanent redirect for the subgroups and projects' do
+ expect(group.redirect_routes.permanent.count).to eq(1)
+ expect(project1.redirect_routes.permanent.count).to eq(1)
+ expect(subgroup1.redirect_routes.permanent.count).to eq(1)
+ expect(nested_subgroup.redirect_routes.permanent.count).to eq(1)
+ expect(nested_project.redirect_routes.permanent.count).to eq(1)
+ end
+ end
+
+ context 'when updating the group goes wrong' do
+ let!(:subgroup1) { create(:group, :public, parent: group) }
+ let!(:subgroup2) { create(:group, :public, parent: group) }
+ let(:new_parent_group) { create(:group, :private) }
+ let!(:project1) { create(:project, :repository, :public, namespace: group) }
+
+ before do
+ allow(group).to receive(:save!).and_raise(ActiveRecord::RecordInvalid.new(group))
+ TestEnv.clean_test_path
+ create(:group_member, :owner, group: new_parent_group, user: user)
+ transfer_service.execute(new_parent_group)
+ end
+
+ it 'should restore group and projects visibility' do
+ subgroup1.reload
+ project1.reload
+ expect(subgroup1.public?).to be_truthy
+ expect(project1.public?).to be_truthy
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 8897a64a138..47c1ebbeb81 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -4,7 +4,7 @@ describe Issues::CloseService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:issue) { create(:issue, assignees: [user2]) }
+ let(:issue) { create(:issue, assignees: [user2], author: create(:user)) }
let(:project) { issue.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb
index f3c98fa5416..322c91065e7 100644
--- a/spec/services/issues/move_service_spec.rb
+++ b/spec/services/issues/move_service_spec.rb
@@ -6,7 +6,7 @@ describe Issues::MoveService do
let(:title) { 'Some issue' }
let(:description) { 'Some issue description' }
let(:old_project) { create(:project) }
- let(:new_project) { create(:project) }
+ let(:new_project) { create(:project, group: create(:group)) }
let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') }
let(:old_issue) do
@@ -250,7 +250,7 @@ describe Issues::MoveService do
context 'issue description with uploads' do
let(:uploader) { build(:file_uploader, project: old_project) }
- let(:description) { "Text and #{uploader.to_markdown}" }
+ let(:description) { "Text and #{uploader.markdown_link}" }
include_context 'issue move executed'
@@ -297,9 +297,11 @@ describe Issues::MoveService do
end
context 'project issue hooks' do
- let(:hook) { create(:project_hook, project: old_project, issues_events: true) }
+ let!(:hook) { create(:project_hook, project: old_project, issues_events: true) }
it 'executes project issue hooks' do
+ allow_any_instance_of(WebHookService).to receive(:execute)
+
# Ideally, we'd test that `WebHookWorker.jobs.size` increased by 1,
# but since the entire spec run takes place in a transaction, we never
# actually get to the `after_commit` hook that queues these jobs.
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 1cb6f2e097f..41237dd7160 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -13,7 +13,8 @@ describe Issues::UpdateService, :mailer do
create(:issue, title: 'Old title',
description: "for #{user2.to_reference}",
assignee_ids: [user3.id],
- project: project)
+ project: project,
+ author: create(:user))
end
before do
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index cb4c3e72aa0..e56d335a7d6 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -172,11 +172,32 @@ describe MergeRequests::BuildService do
end
end
- context 'branch starts with external issue IID followed by a hyphen' do
+ context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do
let(:source_branch) { '12345-fix-issue' }
before do
+ allow(project).to receive(:external_issue_tracker).and_return(false)
+ allow(project).to receive(:issues_enabled?).and_return(false)
+ end
+
+ it 'uses the title of the commit as the title of the merge request' do
+ expect(merge_request.title).to eq(commit_1.safe_message.split("\n").first)
+ end
+
+ it 'uses the description of the commit as the description of the merge request' do
+ commit_description = commit_1.safe_message.split(/\n+/, 2).last
+
+ expect(merge_request.description).to eq("#{commit_description}")
+ end
+ end
+
+ context 'branch starts with JIRA-formatted external issue IID followed by a hyphen' do
+ let(:source_branch) { 'EXMPL-12345-fix-issue' }
+
+ before do
allow(project).to receive(:external_issue_tracker).and_return(true)
+ allow(project).to receive(:issues_enabled?).and_return(false)
+ allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern)
end
it 'uses the title of the commit as the title of the merge request' do
@@ -186,7 +207,7 @@ describe MergeRequests::BuildService do
it 'uses the description of the commit as the description of the merge request and appends the closes text' do
commit_description = commit_1.safe_message.split(/\n+/, 2).last
- expect(merge_request.description).to eq("#{commit_description}\n\nCloses #12345")
+ expect(merge_request.description).to eq("#{commit_description}\n\nCloses EXMPL-12345")
end
end
end
@@ -252,19 +273,46 @@ describe MergeRequests::BuildService do
end
end
- context 'branch starts with external issue IID followed by a hyphen' do
+ context 'branch starts with numeric characters followed by a hyphen with no issue tracker' do
let(:source_branch) { '12345-fix-issue' }
before do
- allow(project).to receive(:external_issue_tracker).and_return(true)
+ allow(project).to receive(:external_issue_tracker).and_return(false)
+ allow(project).to receive(:issues_enabled?).and_return(false)
end
it 'sets the title to the humanized branch title' do
expect(merge_request.title).to eq('12345 fix issue')
end
+ end
+
+ context 'branch starts with JIRA-formatted external issue IID' do
+ let(:source_branch) { 'EXMPL-12345' }
+
+ before do
+ allow(project).to receive(:external_issue_tracker).and_return(true)
+ allow(project).to receive(:issues_enabled?).and_return(false)
+ allow(project).to receive(:external_issue_reference_pattern).and_return(IssueTrackerService.reference_pattern)
+ end
+
+ it 'sets the title to the humanized branch title' do
+ expect(merge_request.title).to eq('Resolve EXMPL-12345')
+ end
it 'appends the closes text' do
- expect(merge_request.description).to eq('Closes #12345')
+ expect(merge_request.description).to eq('Closes EXMPL-12345')
+ end
+
+ context 'followed by hyphenated text' do
+ let(:source_branch) { 'EXMPL-12345-fix-issue' }
+
+ it 'sets the title to the humanized branch title' do
+ expect(merge_request.title).to eq('Resolve EXMPL-12345 "Fix issue"')
+ end
+
+ it 'appends the closes text' do
+ expect(merge_request.description).to eq('Closes EXMPL-12345')
+ 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 4d12de3ecce..216e0cd4266 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -4,7 +4,7 @@ describe MergeRequests::CloseService do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:guest) { create(:user) }
- let(:merge_request) { create(:merge_request, assignee: user2) }
+ let(:merge_request) { create(:merge_request, assignee: user2, author: create(:user)) }
let(:project) { merge_request.project }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) }
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index aa90feeef89..5ef6365fcc9 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -7,7 +7,8 @@ describe MergeRequests::FfMergeService do
create(:merge_request,
source_branch: 'flatten-dir',
target_branch: 'improve/awesome',
- assignee: user2)
+ assignee: user2,
+ author: create(:user))
end
let(:project) { merge_request.project }
diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
index f17db70faf6..240aa638f79 100644
--- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb
@@ -43,7 +43,7 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do
it 'creates a system note' do
note = merge_request.notes.last
- expect(note.note).to match /enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{8}/
+ expect(note.note).to match %r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{8}}
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 7a01d3dd698..903aa0a5078 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -55,11 +55,12 @@ describe MergeRequests::RefreshService do
before do
allow(refresh_service).to receive(:execute_hooks)
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
- reload_mrs
end
it 'executes hooks with update action' do
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+ reload_mrs
+
expect(refresh_service).to have_received(:execute_hooks)
.with(@merge_request, 'update', old_rev: @oldrev)
@@ -72,6 +73,34 @@ describe MergeRequests::RefreshService do
expect(@build_failed_todo).to be_done
expect(@fork_build_failed_todo).to be_done
end
+
+ it 'reloads source branch MRs memoization' do
+ refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
+
+ expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }.to change {
+ refresh_service.instance_variable_get("@source_merge_requests").first.merge_request_diff
+ }
+ end
+
+ context 'when source branch ref does not exists' do
+ before do
+ DeleteBranchService.new(@project, @user).execute(@merge_request.source_branch)
+ end
+
+ it 'closes MRs without source branch ref' do
+ expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }
+ .to change { @merge_request.reload.state }
+ .from('opened')
+ .to('closed')
+
+ expect(@fork_merge_request.reload).to be_open
+ end
+
+ it 'does not change the merge request diff' do
+ expect { refresh_service.execute(@oldrev, @newrev, 'refs/heads/master') }
+ .not_to change { @merge_request.reload.merge_request_diff }
+ end
+ end
end
context 'when pipeline exists for the source branch' do
@@ -371,37 +400,21 @@ describe MergeRequests::RefreshService do
end
it 'references the commit that caused the Work in Progress status' do
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/master')
- allow(refresh_service).to receive(:find_new_commits)
- refresh_service.instance_variable_set("@commits", [
- double(
- id: 'aaaaaaa',
- sha: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e',
- short_id: 'aaaaaaa',
- title: 'Fix issue',
- work_in_progress?: false
- ),
- double(
- id: 'bbbbbbb',
- sha: '498214de67004b1da3d820901307bed2a68a8ef6',
- short_id: 'bbbbbbb',
- title: 'fixup! Fix issue',
- work_in_progress?: true,
- to_reference: 'bbbbbbb'
- ),
- double(
- id: 'ccccccc',
- sha: '1b12f15a11fc6e62177bef08f47bc7b5ce50b141',
- short_id: 'ccccccc',
- title: 'fixup! Fix issue',
- work_in_progress?: true,
- to_reference: 'ccccccc'
- )
- ])
- refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip')
- reload_mrs
- expect(@merge_request.notes.last.note).to eq(
- "marked as a **Work In Progress** from bbbbbbb"
+ wip_merge_request = create(:merge_request,
+ source_project: @project,
+ source_branch: 'wip',
+ target_branch: 'master',
+ target_project: @project)
+
+ commits = wip_merge_request.commits
+ oldrev = commits.last.id
+ newrev = commits.first.id
+ wip_commit = wip_merge_request.commits.find(&:work_in_progress?)
+
+ refresh_service.execute(oldrev, newrev, 'refs/heads/wip')
+
+ expect(wip_merge_request.reload.notes.last.note).to eq(
+ "marked as a **Work In Progress** from #{wip_commit.id}"
)
end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index a44d63e5f9f..9ee37c51d95 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -4,7 +4,7 @@ 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) }
+ let(:merge_request) { create(:merge_request, :closed, assignee: user2, author: create(:user)) }
let(:project) { merge_request.project }
before do
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 2238da2d14d..c31259239ee 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -12,7 +12,8 @@ describe MergeRequests::UpdateService, :mailer do
create(:merge_request, :simple, title: 'Old title',
description: "FYI #{user2.to_reference}",
assignee_id: user3.id,
- source_project: project)
+ source_project: project,
+ author: create(:user))
end
before do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 5c59455e3e1..35eb84e5e88 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -458,7 +458,7 @@ 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) }
+ let(:merge_request) { create(:merge_request, source_project: project, assignee: user, author: create(:user)) }
let(:note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request) }
before do
@@ -469,11 +469,13 @@ describe NotificationService, :mailer do
describe '#new_note' do
it "records sent notifications" do
- # Ensure create SentNotification by noteable = merge_request 6 times, not noteable = note
+ # 3 SentNotification are sent: the MR assignee and author, and the @u_watcher
expect(SentNotification).to receive(:record_note).with(note, any_args).exactly(3).times.and_call_original
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)
expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id)
end
end
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 50e59954f73..15699574b3a 100644
--- a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
+++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb
@@ -6,7 +6,7 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
let(:legacy_storage) { Storage::LegacyProject.new(project) }
let(:hashed_storage) { Storage::HashedProject.new(project) }
- let!(:upload) { Upload.find_by(path: file_uploader.relative_path) }
+ let!(:upload) { Upload.find_by(path: file_uploader.upload_path) }
let(:file_uploader) { build(:file_uploader, project: project) }
let(:old_path) { File.join(base_path(legacy_storage), upload.path) }
let(:new_path) { File.join(base_path(hashed_storage), upload.path) }
@@ -58,6 +58,6 @@ describe Projects::HashedStorage::MigrateAttachmentsService do
end
def base_path(storage)
- FileUploader.dynamic_path_builder(storage.disk_path)
+ File.join(FileUploader.root, storage.disk_path)
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index ab3a257f36f..5b5edc1aa0d 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -54,10 +54,11 @@ describe SystemNoteService do
expect(note_lines[0]).to eq "added #{new_commits.size} commits"
end
- it 'adds a message line for each commit' do
- new_commits.each_with_index do |commit, i|
- # Skip the header
- expect(HTMLEntities.new.decode(note_lines[i + 1])).to eq "* #{commit.short_id} - #{commit.title}"
+ it 'adds a message for each commit' do
+ decoded_note_content = HTMLEntities.new.decode(subject.note)
+
+ new_commits.each do |commit|
+ expect(decoded_note_content).to include("<li>#{commit.short_id} - #{commit.title}</li>")
end
end
end
@@ -69,7 +70,7 @@ describe SystemNoteService do
let(:old_commits) { [noteable.commits.last] }
it 'includes the existing commit' do
- expect(summary_line).to eq "* #{old_commits.first.short_id} - 1 commit from branch `feature`"
+ expect(summary_line).to start_with("<ul><li>#{old_commits.first.short_id} - 1 commit from branch <code>feature</code>")
end
end
@@ -79,22 +80,16 @@ describe SystemNoteService do
context 'with oldrev' do
let(:oldrev) { noteable.commits[2].id }
- it 'includes a commit range' do
- expect(summary_line).to start_with "* #{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id}"
- end
-
- it 'includes a commit count' do
- expect(summary_line).to end_with " - 26 commits from branch `feature`"
+ it 'includes a commit range and count' do
+ expect(summary_line)
+ .to start_with("<ul><li>#{Commit.truncate_sha(oldrev)}...#{old_commits.last.short_id} - 26 commits from branch <code>feature</code>")
end
end
context 'without oldrev' do
- it 'includes a commit range' do
- expect(summary_line).to start_with "* #{old_commits[0].short_id}..#{old_commits[-1].short_id}"
- end
-
- it 'includes a commit count' do
- expect(summary_line).to end_with " - 26 commits from branch `feature`"
+ it 'includes a commit range and count' do
+ expect(summary_line)
+ .to start_with("<ul><li>#{old_commits[0].short_id}..#{old_commits[-1].short_id} - 26 commits from branch <code>feature</code>")
end
end
@@ -104,7 +99,7 @@ describe SystemNoteService do
end
it 'includes the project namespace' do
- expect(summary_line).to end_with "`#{noteable.target_project_namespace}:feature`"
+ expect(summary_line).to include("<code>#{noteable.target_project_namespace}:feature</code>")
end
end
end
@@ -308,7 +303,7 @@ describe SystemNoteService do
end
it "posts the 'merge when pipeline succeeds' system note" do
- expect(subject.note).to match(/enabled an automatic merge when the pipeline for (\w+\/\w+@)?\h{40} succeeds/)
+ expect(subject.note).to match(%r{enabled an automatic merge when the pipeline for (\w+/\w+@)?\h{40} succeeds})
end
end
@@ -693,9 +688,9 @@ describe SystemNoteService do
describe '.new_commit_summary' do
it 'escapes HTML titles' do
commit = double(title: '<pre>This is a test</pre>', short_id: '12345678')
- escaped = '&lt;pre&gt;This is a test&lt;&#x2F;pre&gt;'
+ escaped = '&lt;pre&gt;This is a test&lt;/pre&gt;'
- expect(described_class.new_commit_summary([commit])).to all(match(%r[- #{escaped}]))
+ expect(described_class.new_commit_summary([commit])).to all(match(/- #{escaped}/))
end
end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index edaee03ea6c..1809ae1d141 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -1,10 +1,26 @@
+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(:truncation, cache_tables: false)
+ DatabaseCleaner.clean_with(:deletion, cache_tables: false)
end
config.before(:each) do
@@ -12,15 +28,15 @@ RSpec.configure do |config|
end
config.before(:each, :js) do
- DatabaseCleaner.strategy = :truncation
+ DatabaseCleaner.strategy = :deletion
end
- config.before(:each, :truncate) do
- DatabaseCleaner.strategy = :truncation
+ config.before(:each, :delete) do
+ DatabaseCleaner.strategy = :deletion
end
config.before(:each, :migration) do
- DatabaseCleaner.strategy = :truncation, { cache_tables: false }
+ DatabaseCleaner.strategy = :deletion, { cache_tables: false }
end
config.before(:each) do
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index fa94aa2ae3d..c8662d41769 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -143,15 +143,17 @@ shared_examples 'discussion comments' do |resource_name|
end
if resource_name == 'merge request'
+ let(:note_id) { find("#{comments_selector} .note", match: :first)['data-note-id'] }
+
it 'shows resolved discussion when toggled' do
click_button "Resolve discussion"
- expect(page).to have_selector('.note-row-1', visible: true)
+ expect(page).to have_selector(".note-row-#{note_id}", visible: true)
refresh
click_button "Toggle discussion"
- expect(page).to have_selector('.note-row-1', visible: true)
+ expect(page).to have_selector(".note-row-#{note_id}", visible: true)
end
end
end
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
index 923c8080e6c..2197bc9d853 100644
--- a/spec/support/javascript_fixtures_helpers.rb
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -1,6 +1,5 @@
require 'action_dispatch/testing/test_request'
require 'fileutils'
-require 'gitlab/popen'
module JavaScriptFixturesHelpers
include Gitlab::Popen
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index d12b2757427..ec4ec6f4038 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -190,6 +190,27 @@ module MarkdownMatchers
expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4')
end
end
+
+ # ColorFilter
+ matcher :parse_colors do
+ set_default_markdown_messages
+
+ match do |actual|
+ color_chips = actual.css('code > span.gfm-color_chip > span')
+
+ expect(color_chips.count).to eq(9)
+
+ [
+ '#F00', '#F00A', '#FF0000', '#FF0000AA', 'RGB(0,255,0)',
+ 'RGB(0%,100%,0%)', 'RGBA(0,255,0,0.7)', 'HSL(540,70%,50%)',
+ 'HSLA(540,70%,50%,0.7)'
+ ].each_with_index do |color, i|
+ parsed_color = Banzai::ColorParser.parse(color)
+ expect(color_chips[i]['style']).to match("background-color: #{parsed_color};")
+ expect(color_chips[i].parent.parent.content).to match(color)
+ end
+ end
+ end
end
# Monkeypatch the matcher DSL so that we can reduce some noisy duplication for
diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
index 935c08221e0..7ce80c82439 100644
--- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb
@@ -2,6 +2,8 @@ shared_examples 'handle uploads' do
let(:user) { create(:user) }
let(:jpg) { fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') }
let(:txt) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') }
+ let(:secret) { FileUploader.generate_secret }
+ let(:uploader_class) { FileUploader }
describe "POST #create" do
context 'when a user is not authorized to upload a file' do
@@ -65,7 +67,12 @@ shared_examples 'handle uploads' do
describe "GET #show" do
let(:show_upload) do
- get :show, params.merge(secret: "123456", filename: "image.jpg")
+ get :show, params.merge(secret: secret, filename: "rails_sample.jpg")
+ end
+
+ before do
+ expect(FileUploader).to receive(:generate_secret).and_return(secret)
+ UploadService.new(model, jpg, uploader_class).execute
end
context "when the model is public" do
@@ -75,11 +82,6 @@ shared_examples 'handle uploads' do
context "when not signed in" do
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
it "responds with status 200" do
show_upload
@@ -88,6 +90,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
@@ -102,11 +108,6 @@ shared_examples 'handle uploads' do
end
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
it "responds with status 200" do
show_upload
@@ -115,6 +116,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
@@ -131,11 +136,6 @@ shared_examples 'handle uploads' do
context "when not signed in" do
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
context "when the file is an image" do
before do
allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
@@ -149,6 +149,10 @@ shared_examples 'handle uploads' do
end
context "when the file is not an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(false)
+ end
+
it "redirects to the sign in page" do
show_upload
@@ -158,6 +162,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "redirects to the sign in page" do
show_upload
@@ -177,11 +185,6 @@ shared_examples 'handle uploads' do
end
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
it "responds with status 200" do
show_upload
@@ -190,6 +193,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
@@ -200,11 +207,6 @@ shared_examples 'handle uploads' do
context "when the user doesn't have access to the model" do
context "when the file exists" do
- before do
- allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
- allow(jpg).to receive(:exists?).and_return(true)
- end
-
context "when the file is an image" do
before do
allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
@@ -218,6 +220,10 @@ shared_examples 'handle uploads' do
end
context "when the file is not an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
@@ -227,6 +233,10 @@ shared_examples 'handle uploads' do
end
context "when the file doesn't exist" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false)
+ end
+
it "responds with status 404" do
show_upload
diff --git a/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
new file mode 100644
index 00000000000..934d53e7bba
--- /dev/null
+++ b/spec/support/shared_examples/uploaders/gitlab_uploader_shared_examples.rb
@@ -0,0 +1,48 @@
+shared_examples "matches the method pattern" do |method|
+ let(:target) { subject }
+ let(:args) { nil }
+ let(:pattern) { patterns[method] }
+
+ it do
+ return skip "No pattern provided, skipping." unless pattern
+
+ expect(target.method(method).call(*args)).to match(pattern)
+ end
+end
+
+shared_examples "builds correct paths" do |**patterns|
+ let(:patterns) { patterns }
+
+ before do
+ allow(subject).to receive(:filename).and_return('<filename>')
+ end
+
+ describe "#store_dir" do
+ it_behaves_like "matches the method pattern", :store_dir
+ end
+
+ describe "#cache_dir" do
+ it_behaves_like "matches the method pattern", :cache_dir
+ end
+
+ describe "#work_dir" do
+ it_behaves_like "matches the method pattern", :work_dir
+ end
+
+ describe "#upload_path" do
+ it_behaves_like "matches the method pattern", :upload_path
+ end
+
+ describe ".absolute_path" do
+ it_behaves_like "matches the method pattern", :absolute_path do
+ let(:target) { subject.class }
+ let(:args) { [upload] }
+ end
+ end
+
+ describe ".base_dir" do
+ it_behaves_like "matches the method pattern", :base_dir do
+ let(:target) { subject.class }
+ end
+ end
+end
diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb
index f9121cce985..52e47ae2d34 100644
--- a/spec/support/stored_repositories.rb
+++ b/spec/support/stored_repositories.rb
@@ -15,9 +15,7 @@ RSpec.configure do |config|
# Track the maximum number of failures
first_failure = Time.parse("2017-11-14 17:52:30")
last_failure = Time.parse("2017-11-14 18:54:37")
- failure_count = Gitlab::CurrentSettings
- .current_application_settings
- .circuitbreaker_failure_count_threshold + 1
+ failure_count = Gitlab::CurrentSettings.circuitbreaker_failure_count_threshold + 1
cache_key = "#{Gitlab::Git::Storage::REDIS_KEY_PREFIX}broken:#{Gitlab::Environment.hostname}"
Gitlab::Git::Storage.redis.with do |redis|
diff --git a/spec/support/stub_env.rb b/spec/support/stub_env.rb
index 695152e2d4e..36b90fc68d6 100644
--- a/spec/support/stub_env.rb
+++ b/spec/support/stub_env.rb
@@ -1,7 +1,5 @@
# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
module StubENV
- include Gitlab::CurrentSettings
-
def stub_env(key_or_hash, value = nil)
init_stub unless env_stubbed?
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index fd6368e7b40..c275522159c 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -20,7 +20,7 @@ module TestEnv
'improve/awesome' => '5937ac0',
'merged-target' => '21751bf',
'markdown' => '0ed8c6c',
- 'lfs' => 'be93687',
+ 'lfs' => '55bc176',
'master' => 'b83d6e3',
'merge-test' => '5937ac0',
"'test'" => 'e56497b',
@@ -237,7 +237,7 @@ module TestEnv
end
def artifacts_path
- Gitlab.config.artifacts.path
+ Gitlab.config.artifacts.storage_path
end
# When no cached assets exist, manually hit the root path to create them
diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb
index d05eda08201..5752078d2a0 100644
--- a/spec/support/track_untracked_uploads_helpers.rb
+++ b/spec/support/track_untracked_uploads_helpers.rb
@@ -1,6 +1,6 @@
module TrackUntrackedUploadsHelpers
def uploaded_file
- fixture_path = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
+ fixture_path = Rails.root.join('spec/fixtures/rails_sample.jpg')
fixture_file_upload(fixture_path)
end
diff --git a/spec/support/unique_ip_check_shared_examples.rb b/spec/support/unique_ip_check_shared_examples.rb
index 3d9705c9c05..e5c8ac6a004 100644
--- a/spec/support/unique_ip_check_shared_examples.rb
+++ b/spec/support/unique_ip_check_shared_examples.rb
@@ -9,7 +9,7 @@ shared_context 'unique ips sign in limit' do
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
- current_application_settings.update!(
+ Gitlab::CurrentSettings.update!(
unique_ips_limit_enabled: true,
unique_ips_limit_time_window: 10000
)
@@ -34,7 +34,7 @@ end
shared_examples 'user login operation with unique ip limit' do
include_context 'unique ips sign in limit' do
before do
- current_application_settings.update!(unique_ips_limit_per_user: 1)
+ Gitlab::CurrentSettings.update!(unique_ips_limit_per_user: 1)
end
it 'allows user authenticating from the same ip' do
@@ -52,7 +52,7 @@ end
shared_examples 'user login request with unique ip limit' do |success_status = 200|
include_context 'unique ips sign in limit' do
before do
- current_application_settings.update!(unique_ips_limit_per_user: 1)
+ Gitlab::CurrentSettings.update!(unique_ips_limit_per_user: 1)
end
it 'allows user authenticating from the same ip' do
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index b41c3b3958a..168facd51a6 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -165,7 +165,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('pages.tar.gz')
expect(tar_contents).to match('lfs.tar.gz')
expect(tar_contents).to match('registry.tar.gz')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)\/$/)
+ expect(tar_contents).not_to match(%r{^.{4,9}[rwx].* (database.sql.gz|uploads.tar.gz|repositories|builds.tar.gz|pages.tar.gz|artifacts.tar.gz|registry.tar.gz)/$})
end
it 'deletes temp directories' do
diff --git a/spec/tasks/gitlab/git_rake_spec.rb b/spec/tasks/gitlab/git_rake_spec.rb
index dacc5dc5ae7..9aebf7b0b4a 100644
--- a/spec/tasks/gitlab/git_rake_spec.rb
+++ b/spec/tasks/gitlab/git_rake_spec.rb
@@ -19,7 +19,7 @@ describe 'gitlab:git rake tasks' do
describe 'fsck' do
it 'outputs the integrity check for a repo' do
- expect { run_rake_task('gitlab:git:fsck') }.to output(/Performed Checking integrity at .*@hashed\/1\/2\/test.git/).to_stdout
+ expect { run_rake_task('gitlab:git:fsck') }.to output(%r{Performed Checking integrity at .*@hashed/1/2/test.git}).to_stdout
end
it 'errors out about config.lock issues' do
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 6aba86fdc3c..b37d6ac831f 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -76,7 +76,11 @@ describe 'gitlab:gitaly namespace rake task' do
end
context 'when Rails.env is test' do
- let(:command) { %w[make BUNDLE_FLAGS=--no-deployment] }
+ let(:command) do
+ %W[make
+ BUNDLE_FLAGS=--no-deployment
+ BUNDLE_PATH=#{Bundler.bundle_path}]
+ end
before do
allow(Rails.env).to receive(:test?).and_return(true)
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
index fae5ec35c47..e9322ec4931 100644
--- a/spec/tasks/gitlab/task_helpers_spec.rb
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require 'tasks/gitlab/task_helpers'
class TestHelpersTest
include Gitlab::TaskHelpers
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
index 04ee6e9bfad..091ba824fc6 100644
--- a/spec/uploaders/attachment_uploader_spec.rb
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -1,28 +1,14 @@
require 'spec_helper'
describe AttachmentUploader do
- let(:uploader) { described_class.new(build_stubbed(:user)) }
+ let(:note) { create(:note, :with_attachment) }
+ let(:uploader) { note.attachment }
+ let(:upload) { create(:upload, :attachment_upload, model: uploader.model) }
- describe "#store_dir" do
- it "stores in the system dir" do
- expect(uploader.store_dir).to start_with("uploads/-/system/user")
- end
+ subject { uploader }
- it "uses the old path when using object storage" do
- expect(described_class).to receive(:file_storage?).and_return(false)
- expect(uploader.store_dir).to start_with("uploads/user")
- end
- end
-
- describe '#move_to_cache' do
- it 'is true' do
- expect(uploader.move_to_cache).to eq(true)
- end
- end
-
- describe '#move_to_store' do
- it 'is true' do
- expect(uploader.move_to_store).to eq(true)
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/note/attachment/],
+ upload_path: %r[uploads/-/system/note/attachment/],
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/note/attachment/]
end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
index 1dc574699d8..bf9028c9260 100644
--- a/spec/uploaders/avatar_uploader_spec.rb
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -1,18 +1,16 @@
require 'spec_helper'
describe AvatarUploader do
- let(:uploader) { described_class.new(build_stubbed(:user)) }
+ let(:model) { create(:user, :with_avatar) }
+ let(:uploader) { described_class.new(model, :avatar) }
+ let(:upload) { create(:upload, model: model) }
- describe "#store_dir" do
- it "stores in the system dir" do
- expect(uploader.store_dir).to start_with("uploads/-/system/user")
- end
+ subject { uploader }
- it "uses the old path when using object storage" do
- expect(described_class).to receive(:file_storage?).and_return(false)
- expect(uploader.store_dir).to start_with("uploads/user")
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/user/avatar/],
+ upload_path: %r[uploads/-/system/user/avatar/],
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/user/avatar/]
describe '#move_to_cache' do
it 'is false' do
diff --git a/spec/uploaders/file_mover_spec.rb b/spec/uploaders/file_mover_spec.rb
index 0cf462e9553..bc024cd307c 100644
--- a/spec/uploaders/file_mover_spec.rb
+++ b/spec/uploaders/file_mover_spec.rb
@@ -3,13 +3,13 @@ require 'spec_helper'
describe FileMover do
let(:filename) { 'banana_sample.gif' }
let(:file) { fixture_file_upload(Rails.root.join('spec', 'fixtures', filename)) }
+ let(:temp_file_path) { File.join('uploads/-/system/temp', 'secret55', filename) }
+
let(:temp_description) do
- 'test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) same ![banana_sample]'\
- '(/uploads/-/system/temp/secret55/banana_sample.gif)'
+ "test ![banana_sample](/#{temp_file_path}) "\
+ "same ![banana_sample](/#{temp_file_path}) "
end
- let(:temp_file_path) { File.join('secret55', filename).to_s }
- let(:file_path) { File.join('uploads', '-', 'system', 'personal_snippet', snippet.id.to_s, 'secret55', filename).to_s }
-
+ 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 }
@@ -28,8 +28,8 @@ describe FileMover do
expect(snippet.reload.description)
.to eq(
- "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"\
- " same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif)"
+ "test ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "\
+ "same ![banana_sample](/uploads/-/system/personal_snippet/#{snippet.id}/secret55/banana_sample.gif) "
)
end
@@ -50,8 +50,8 @@ describe FileMover do
expect(snippet.reload.description)
.to eq(
- "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"\
- " same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif)"
+ "test ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) "\
+ "same ![banana_sample](/uploads/-/system/temp/secret55/banana_sample.gif) "
)
end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index fd195d6f9b8..a559681a079 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -1,118 +1,84 @@
require 'spec_helper'
describe FileUploader do
- let(:uploader) { described_class.new(build_stubbed(:project)) }
+ let(:group) { create(:group, name: 'awesome') }
+ let(:project) { create(:project, namespace: group, name: 'project') }
+ let(:uploader) { described_class.new(project) }
+ let(:upload) { double(model: project, path: 'secret/foo.jpg') }
- context 'legacy storage' do
- let(:project) { build_stubbed(:project) }
-
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- upload = double(model: project, path: 'secret/foo.jpg')
-
- dynamic_segment = project.full_path
-
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
- end
- end
+ subject { uploader }
- describe "#store_dir" do
- it "stores in the namespace path" do
- uploader = described_class.new(project)
-
- expect(uploader.store_dir).to include(project.full_path)
- expect(uploader.store_dir).not_to include("system")
- end
- end
+ shared_examples 'builds correct legacy storage paths' do
+ include_examples 'builds correct paths',
+ store_dir: %r{awesome/project/\h+},
+ absolute_path: %r{#{described_class.root}/awesome/project/secret/foo.jpg}
end
- context 'hashed storage' do
+ shared_examples 'uses hashed storage' do
context 'when rolled out attachments' do
- let(:project) { build_stubbed(:project, :hashed) }
-
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- upload = double(model: project, path: 'secret/foo.jpg')
-
- dynamic_segment = project.disk_path
-
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
- end
+ before do
+ allow(project).to receive(:disk_path).and_return('ca/fe/fe/ed')
end
- describe "#store_dir" do
- it "stores in the namespace path" do
- uploader = described_class.new(project)
+ let(:project) { build_stubbed(:project, :hashed, namespace: group, name: 'project') }
- expect(uploader.store_dir).to include(project.disk_path)
- expect(uploader.store_dir).not_to include("system")
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r{ca/fe/fe/ed/\h+},
+ absolute_path: %r{#{described_class.root}/ca/fe/fe/ed/secret/foo.jpg}
end
context 'when only repositories are rolled out' do
- let(:project) { build_stubbed(:project, storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
-
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- upload = double(model: project, path: 'secret/foo.jpg')
+ let(:project) { build_stubbed(:project, namespace: group, name: 'project', storage_version: Project::HASHED_STORAGE_FEATURES[:repository]) }
- dynamic_segment = project.full_path
+ it_behaves_like 'builds correct legacy storage paths'
+ end
+ end
- expect(described_class.absolute_path(upload))
- .to end_with("#{dynamic_segment}/secret/foo.jpg")
- end
- end
+ context 'legacy storage' do
+ it_behaves_like 'builds correct legacy storage paths'
+ include_examples 'uses hashed storage'
+ end
- describe "#store_dir" do
- it "stores in the namespace path" do
- uploader = described_class.new(project)
+ describe 'initialize' do
+ let(:uploader) { described_class.new(double, secret: 'secret') }
- expect(uploader.store_dir).to include(project.full_path)
- expect(uploader.store_dir).not_to include("system")
- end
- end
+ it 'accepts a secret parameter' do
+ expect(described_class).not_to receive(:generate_secret)
+ expect(uploader.secret).to eq('secret')
end
end
- describe 'initialize' do
+ describe '#secret' do
it 'generates a secret if none is provided' do
- expect(SecureRandom).to receive(:hex).and_return('secret')
-
- uploader = described_class.new(double)
-
- expect(uploader.secret).to eq 'secret'
+ expect(described_class).to receive(:generate_secret).and_return('secret')
+ expect(uploader.secret).to eq('secret')
end
+ end
- it 'accepts a secret parameter' do
- expect(SecureRandom).not_to receive(:hex)
+ describe '#upload=' do
+ let(:secret) { SecureRandom.hex }
+ let(:upload) { create(:upload, :issuable_upload, secret: secret, filename: 'file.txt') }
- uploader = described_class.new(double, 'secret')
+ it 'handles nil' do
+ expect(uploader).not_to receive(:apply_context!)
- expect(uploader.secret).to eq 'secret'
+ uploader.upload = nil
end
- end
- describe '#move_to_cache' do
- it 'is true' do
- expect(uploader.move_to_cache).to eq(true)
- end
- end
+ it 'extract the uploader context from it' do
+ expect(uploader).to receive(:apply_context!).with(a_hash_including(secret: secret, identifier: 'file.txt'))
- describe '#move_to_store' do
- it 'is true' do
- expect(uploader.move_to_store).to eq(true)
+ uploader.upload = upload
end
- end
- describe '#relative_path' do
- it 'removes the leading dynamic path segment' do
- fixture = Rails.root.join('spec', 'fixtures', 'rails_sample.jpg')
- uploader.store!(fixture_file_upload(fixture))
+ context 'uploader_context is empty' do
+ it 'fallbacks to regex based extraction' do
+ expect(upload).to receive(:uploader_context).and_return({})
- expect(uploader.relative_path).to match(/\A\h{32}\/rails_sample.jpg\z/)
+ uploader.upload = upload
+ expect(uploader.secret).to eq(secret)
+ expect(uploader.instance_variable_get(:@identifier)).to eq('file.txt')
+ end
end
end
end
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
index a144b39f74f..60e35dcf235 100644
--- a/spec/uploaders/gitlab_uploader_spec.rb
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -4,7 +4,7 @@ require 'carrierwave/storage/fog'
describe GitlabUploader do
let(:uploader_class) { Class.new(described_class) }
- subject { uploader_class.new }
+ subject { uploader_class.new(double) }
describe '#file_storage?' do
context 'when file storage is used' do
diff --git a/spec/uploaders/job_artifact_uploader_spec.rb b/spec/uploaders/job_artifact_uploader_spec.rb
index 14fd5f3600f..d606404e95d 100644
--- a/spec/uploaders/job_artifact_uploader_spec.rb
+++ b/spec/uploaders/job_artifact_uploader_spec.rb
@@ -3,33 +3,13 @@ require 'spec_helper'
describe JobArtifactUploader do
let(:job_artifact) { create(:ci_job_artifact) }
let(:uploader) { described_class.new(job_artifact, :file) }
- let(:local_path) { Gitlab.config.artifacts.path }
- describe '#store_dir' do
- subject { uploader.store_dir }
+ subject { uploader }
- let(:path) { "#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/#{job_artifact.project_id}/#{job_artifact.id}" }
-
- context 'when using local storage' do
- it { is_expected.to start_with(local_path) }
- it { is_expected.to match(/\h{2}\/\h{2}\/\h{64}\/\d{4}_\d{1,2}_\d{1,2}\/\d+\/\d+\z/) }
- it { is_expected.to end_with(path) }
- end
- end
-
- describe '#cache_dir' do
- subject { uploader.cache_dir }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('/tmp/cache') }
- end
-
- describe '#work_dir' do
- subject { uploader.work_dir }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('/tmp/work') }
- end
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}/\h{64}/\d{4}_\d{1,2}_\d{1,2}/\d+/\d+\z],
+ cache_dir: %r[artifacts/tmp/cache],
+ work_dir: %r[artifacts/tmp/work]
context 'file is stored in valid local_path' do
let(:file) do
@@ -43,9 +23,9 @@ describe JobArtifactUploader do
subject { uploader.file.path }
- it { is_expected.to start_with(local_path) }
+ it { is_expected.to start_with("#{uploader.root}/#{uploader.class.base_dir}") }
it { is_expected.to include("/#{job_artifact.created_at.utc.strftime('%Y_%m_%d')}/") }
- it { is_expected.to include("/#{job_artifact.project_id}/") }
+ it { is_expected.to include("/#{job_artifact.job_id}/#{job_artifact.id}/") }
it { is_expected.to end_with("ci_build_artifacts.zip") }
end
end
diff --git a/spec/uploaders/legacy_artifact_uploader_spec.rb b/spec/uploaders/legacy_artifact_uploader_spec.rb
index efeffb78772..54c6a8b869b 100644
--- a/spec/uploaders/legacy_artifact_uploader_spec.rb
+++ b/spec/uploaders/legacy_artifact_uploader_spec.rb
@@ -3,49 +3,22 @@ require 'rails_helper'
describe LegacyArtifactUploader do
let(:job) { create(:ci_build) }
let(:uploader) { described_class.new(job, :legacy_artifacts_file) }
- let(:local_path) { Gitlab.config.artifacts.path }
+ let(:local_path) { described_class.root }
- describe '.local_store_path' do
- subject { described_class.local_store_path }
+ subject { uploader }
- it "delegate to artifacts path" do
- expect(Gitlab.config.artifacts).to receive(:path)
-
- subject
- end
- end
-
- describe '.artifacts_upload_path' do
- subject { described_class.artifacts_upload_path }
+ # 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
-
- describe '#store_dir' do
- subject { uploader.store_dir }
-
- let(:path) { "#{job.created_at.utc.strftime('%Y_%m')}/#{job.project_id}/#{job.id}" }
-
- context 'when using local storage' do
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with(path) }
- end
+ it { is_expected.to end_with('tmp/uploads') }
end
- describe '#cache_dir' do
- subject { uploader.cache_dir }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('/tmp/cache') }
- end
-
- describe '#work_dir' do
- subject { uploader.work_dir }
-
- it { is_expected.to start_with(local_path) }
- it { is_expected.to end_with('/tmp/work') }
- 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]
describe '#filename' do
# we need to use uploader, as this makes to use mounter
@@ -69,7 +42,7 @@ describe LegacyArtifactUploader do
subject { uploader.file.path }
- it { is_expected.to start_with(local_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") }
diff --git a/spec/uploaders/lfs_object_uploader_spec.rb b/spec/uploaders/lfs_object_uploader_spec.rb
index 7088bc23334..6ebc885daa8 100644
--- a/spec/uploaders/lfs_object_uploader_spec.rb
+++ b/spec/uploaders/lfs_object_uploader_spec.rb
@@ -2,39 +2,13 @@ require 'spec_helper'
describe LfsObjectUploader do
let(:lfs_object) { create(:lfs_object, :with_file) }
- let(:uploader) { described_class.new(lfs_object) }
+ let(:uploader) { described_class.new(lfs_object, :file) }
let(:path) { Gitlab.config.lfs.storage_path }
- describe '#move_to_cache' do
- it 'is true' do
- expect(uploader.move_to_cache).to eq(true)
- end
- end
+ subject { uploader }
- describe '#move_to_store' do
- it 'is true' do
- expect(uploader.move_to_store).to eq(true)
- end
- end
-
- describe '#store_dir' do
- subject { uploader.store_dir }
-
- it { is_expected.to start_with(path) }
- it { is_expected.to end_with("#{lfs_object.oid[0, 2]}/#{lfs_object.oid[2, 2]}") }
- end
-
- describe '#cache_dir' do
- subject { uploader.cache_dir }
-
- it { is_expected.to start_with(path) }
- it { is_expected.to end_with('/tmp/cache') }
- end
-
- describe '#work_dir' do
- subject { uploader.work_dir }
-
- it { is_expected.to start_with(path) }
- it { is_expected.to end_with('/tmp/work') }
- end
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}],
+ cache_dir: %r[/lfs-objects/tmp/cache],
+ work_dir: %r[/lfs-objects/tmp/work]
end
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index c6c4500c179..24a2fc0f72e 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -1,21 +1,16 @@
require 'spec_helper'
+IDENTIFIER = %r{\h+/\S+}
+
describe NamespaceFileUploader do
let(:group) { build_stubbed(:group) }
let(:uploader) { described_class.new(group) }
+ let(:upload) { create(:upload, :namespace_upload, model: group) }
- describe "#store_dir" do
- it "stores in the namespace id directory" do
- expect(uploader.store_dir).to include(group.id.to_s)
- end
- end
-
- describe ".absolute_path" do
- it "stores in thecorrect directory" do
- upload_record = create(:upload, :namespace_upload, model: group)
+ subject { uploader }
- expect(described_class.absolute_path(upload_record))
- .to include("-/system/namespace/#{group.id}")
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/namespace/\d+],
+ upload_path: IDENTIFIER,
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}]
end
diff --git a/spec/uploaders/personal_file_uploader_spec.rb b/spec/uploaders/personal_file_uploader_spec.rb
index cbafa9f478d..ed1fba6edda 100644
--- a/spec/uploaders/personal_file_uploader_spec.rb
+++ b/spec/uploaders/personal_file_uploader_spec.rb
@@ -1,25 +1,27 @@
require 'spec_helper'
+IDENTIFIER = %r{\h+/\S+}
+
describe PersonalFileUploader do
- let(:uploader) { described_class.new(build_stubbed(:project)) }
- let(:snippet) { create(:personal_snippet) }
+ let(:model) { create(:personal_snippet) }
+ let(:uploader) { described_class.new(model) }
+ let(:upload) { create(:upload, :personal_snippet_upload) }
- describe '.absolute_path' do
- it 'returns the correct absolute path by building it dynamically' do
- upload = double(model: snippet, path: 'secret/foo.jpg')
+ subject { uploader }
- dynamic_segment = "personal_snippet/#{snippet.id}"
-
- expect(described_class.absolute_path(upload)).to end_with("/-/system/#{dynamic_segment}/secret/foo.jpg")
- end
- end
+ it_behaves_like 'builds correct paths',
+ store_dir: %r[uploads/-/system/personal_snippet/\d+],
+ upload_path: IDENTIFIER,
+ absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}]
describe '#to_h' do
- it 'returns the hass' do
- uploader = described_class.new(snippet, 'secret')
+ before do
+ subject.instance_variable_set(:@secret, 'secret')
+ end
+ it 'is correct' do
allow(uploader).to receive(:file).and_return(double(extension: 'txt', filename: 'file_name'))
- expected_url = "/uploads/-/system/personal_snippet/#{snippet.id}/secret/file_name"
+ expected_url = "/uploads/-/system/personal_snippet/#{model.id}/secret/file_name"
expect(uploader.to_h).to eq(
alt: 'file_name',
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 7ef7fb7d758..9a3e5d83e01 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -3,16 +3,16 @@ require 'rails_helper'
describe RecordsUploads do
let!(:uploader) do
class RecordsUploadsExampleUploader < GitlabUploader
- include RecordsUploads
+ include RecordsUploads::Concern
storage :file
- def model
- FactoryBot.build_stubbed(:user)
+ def dynamic_segment
+ 'co/fe/ee'
end
end
- RecordsUploadsExampleUploader.new
+ RecordsUploadsExampleUploader.new(build_stubbed(:user))
end
def upload_fixture(filename)
@@ -20,48 +20,55 @@ describe RecordsUploads do
end
describe 'callbacks' do
- it 'calls `record_upload` after `store`' do
+ let(:upload) { create(:upload) }
+
+ before do
+ uploader.upload = upload
+ end
+
+ it '#record_upload after `store`' do
expect(uploader).to receive(:record_upload).once
uploader.store!(upload_fixture('doc_sample.txt'))
end
- it 'calls `destroy_upload` after `remove`' do
- expect(uploader).to receive(:destroy_upload).once
-
+ it '#destroy_upload after `remove`' do
uploader.store!(upload_fixture('doc_sample.txt'))
+ expect(uploader).to receive(:destroy_upload).once
uploader.remove!
end
end
describe '#record_upload callback' do
- it 'returns early when not using file storage' do
- allow(uploader).to receive(:file_storage?).and_return(false)
- expect(Upload).not_to receive(:record)
-
- uploader.store!(upload_fixture('rails_sample.jpg'))
+ it 'creates an Upload record after store' do
+ expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.to change { Upload.count }.by(1)
end
- it "returns early when the file doesn't exist" do
- allow(uploader).to receive(:file).and_return(double(exists?: false))
- expect(Upload).not_to receive(:record)
-
+ it 'creates a new record and assigns size, path, model, and uploader' do
uploader.store!(upload_fixture('rails_sample.jpg'))
+
+ upload = uploader.upload
+ aggregate_failures do
+ expect(upload).to be_persisted
+ expect(upload.size).to eq uploader.file.size
+ expect(upload.path).to eq uploader.upload_path
+ expect(upload.model_id).to eq uploader.model.id
+ expect(upload.model_type).to eq uploader.model.class.to_s
+ expect(upload.uploader).to eq uploader.class.to_s
+ end
end
- it 'creates an Upload record after store' do
- expect(Upload).to receive(:record)
- .with(uploader)
+ it "does not create an Upload record when the file doesn't exist" do
+ allow(uploader).to receive(:file).and_return(double(exists?: false))
- uploader.store!(upload_fixture('rails_sample.jpg'))
+ expect { uploader.store!(upload_fixture('rails_sample.jpg')) }.not_to change { Upload.count }
end
it 'does not create an Upload record if model is missing' do
- expect_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil)
- expect(Upload).not_to receive(:record).with(uploader)
+ allow_any_instance_of(RecordsUploadsExampleUploader).to receive(:model).and_return(nil)
- uploader.store!(upload_fixture('rails_sample.jpg'))
+ 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
@@ -72,29 +79,15 @@ describe RecordsUploads do
uploader: uploader.class.to_s
)
+ uploader.upload = existing
uploader.store!(upload_fixture('rails_sample.jpg'))
expect { existing.reload }.to raise_error(ActiveRecord::RecordNotFound)
- expect(Upload.count).to eq 1
+ expect(Upload.count).to eq(1)
end
end
describe '#destroy_upload callback' do
- it 'returns early when not using file storage' do
- uploader.store!(upload_fixture('rails_sample.jpg'))
-
- allow(uploader).to receive(:file_storage?).and_return(false)
- expect(Upload).not_to receive(:remove_path)
-
- uploader.remove!
- end
-
- it 'returns early when file is nil' do
- expect(Upload).not_to receive(:remove_path)
-
- uploader.remove!
- end
-
it 'it destroys Upload records at the same path after removal' do
uploader.store!(upload_fixture('rails_sample.jpg'))
diff --git a/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
new file mode 100644
index 00000000000..6e7d8db99c4
--- /dev/null
+++ b/spec/views/projects/pipeline_schedules/_pipeline_schedule.html.haml_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe 'projects/pipeline_schedules/_pipeline_schedule' do
+ let(:owner) { create(:user) }
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let(:pipeline_schedule) { create(:ci_pipeline_schedule, :nightly, project: project) }
+
+ before do
+ assign(:project, project)
+
+ allow(view).to receive(:current_user).and_return(user)
+ allow(view).to receive(:pipeline_schedule).and_return(pipeline_schedule)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ context 'taking ownership of schedule' do
+ context 'when non-owner is signed in' do
+ let(:user) { master }
+
+ before do
+ allow(view).to receive(:can?).with(master, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(true)
+ end
+
+ it 'non-owner can take ownership of pipeline' do
+ render
+
+ expect(rendered).to have_link('Take ownership')
+ end
+ end
+
+ context 'when owner is signed in' do
+ let(:user) { owner }
+
+ before do
+ allow(view).to receive(:can?).with(owner, :take_ownership_pipeline_schedule, pipeline_schedule).and_return(false)
+ end
+
+ it 'owner cannot take ownership of pipeline' do
+ render
+
+ expect(rendered).not_to have_link('Take ownership')
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
index 95f0be49412..7b300150874 100644
--- a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb
@@ -13,8 +13,8 @@ describe 'projects/pipelines_settings/_show' do
render
expect(rendered).to have_css('.settings-message')
- expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and the')
- expect(rendered).to have_link('Kubernetes service')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a domain name and a')
+ expect(rendered).to have_link('Kubernetes cluster')
end
end
@@ -27,8 +27,8 @@ describe 'projects/pipelines_settings/_show' do
render
expect(rendered).to have_css('.settings-message')
- expect(rendered).to have_text('Auto Review Apps and Auto Deploy need the')
- expect(rendered).to have_link('Kubernetes service')
+ expect(rendered).to have_text('Auto Review Apps and Auto Deploy need a')
+ expect(rendered).to have_link('Kubernetes cluster')
end
end
end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 7274a9f00f9..2b1a617ee62 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -49,9 +49,22 @@ describe RepositoryImportWorker do
expect do
subject.perform(project.id)
- end.to raise_error(StandardError, error)
+ end.to raise_error(RuntimeError, error)
expect(project.reload.import_jid).not_to be_nil
end
+
+ it 'updates the error on Import/Export' do
+ error = %q{remote: Not Found fatal: repository 'https://user:pass@test.com/root/repoC.git/' not found }
+
+ project.update_attributes(import_jid: '123', import_type: 'gitlab_project')
+ expect_any_instance_of(Projects::ImportService).to receive(:execute).and_return({ status: :error, message: error })
+
+ expect do
+ subject.perform(project.id)
+ end.to raise_error(RuntimeError, error)
+
+ expect(project.reload.import_error).not_to be_nil
+ end
end
context 'when using an asynchronous importer' do
diff --git a/spec/workers/upload_checksum_worker_spec.rb b/spec/workers/upload_checksum_worker_spec.rb
index 911360da66c..9e50ce15871 100644
--- a/spec/workers/upload_checksum_worker_spec.rb
+++ b/spec/workers/upload_checksum_worker_spec.rb
@@ -2,18 +2,31 @@ require 'rails_helper'
describe UploadChecksumWorker do
describe '#perform' do
- it 'rescues ActiveRecord::RecordNotFound' do
- expect { described_class.new.perform(999_999) }.not_to raise_error
+ subject { described_class.new }
+
+ context 'without a valid record' do
+ it 'rescues ActiveRecord::RecordNotFound' do
+ expect { subject.perform(999_999) }.not_to raise_error
+ end
end
- it 'calls calculate_checksum_without_delay and save!' do
- upload = spy
- expect(Upload).to receive(:find).with(999_999).and_return(upload)
+ context 'with a valid record' do
+ let(:upload) { create(:user, :with_avatar).avatar.upload }
+
+ before do
+ expect(Upload).to receive(:find).and_return(upload)
+ allow(upload).to receive(:foreground_checksumable?).and_return(false)
+ end
- described_class.new.perform(999_999)
+ it 'calls calculate_checksum!' do
+ expect(upload).to receive(:calculate_checksum!)
+ subject.perform(upload.id)
+ end
- expect(upload).to have_received(:calculate_checksum)
- expect(upload).to have_received(:save!)
+ it 'calls save!' do
+ expect(upload).to receive(:save!)
+ subject.perform(upload.id)
+ end
end
end
end